diff --git a/README.MD b/README.MD index f78940a10..8484924b4 100644 --- a/README.MD +++ b/README.MD @@ -1,19 +1,43 @@ # Angelica -the little angle that supports shaders while saving your fps from a certain death - boubou +* The little Angle [sic] that supports shaders while saving your fps from a certain death -boubou +* Is it obtuse, or accute? -Caedis +* It's a cute angle -mitchej123 -Not yet in an usable state +**Alpha Quality - Here there be Dragons** -Original build instructions obtained via minecraftforums archeology: -``` -Developing environment - - Extract forge src to forge folder. Run "gradlew setupDecompWorkspace setupDevWorkspace" until it succeeded, then "gradlew build", then "gradlew eclipse". - - Delete everything in src\main folder inside forge folder. - - Extract smc-2.3.18-mc1.7.10-src.7z to src\main folder. - - Read patch file shadersmod-mcf.patch - - Find %userprofile%\.gradle\caches\minecraft\net\minecraftforge\forge\*\forgeSrc-*-sources.jar and extract it somewhere. - -- Select files that need to be patched and copy them to src\main with correct folder structure. - - Apply patch shadersmod-mcf.patch - - In eclipse, add java_dev folder as another source folder. - - In eclipse, exclude InitNames in src folder from build path. Use InitNames in java_dev folder instead. - - Find %userprofile%\.gradle\caches\modules-2\files-2.1\net.minecraftforge\forge\*\*\forge-*-userdev.jar and extract conf\*.csv and conf\*.srg in it to forge\conf and rename packaged.srg to joined.srg. -``` +# Known (temporary) Incompatibilities +* Forge Relocation - Currently non Thread Safe; safe if not in use +* ProjectRed Frames - Currently non Thread Safe; safe if not in use +* EndlessIDs - Future compat planned +* RPLE - Future compat planned + +# Permanent Incompatibilities +* Optifine [Disabled, won't fix] +* Fastcraft [Disable, won't fix] +* ArchaicFix 0.6.2 and below - Use 0.7.0 or above +* Hodgepodge 2.4.2 and below - Use 2.4.3 or above +* CodeChickenCore/Lib <1.2.0 - Threading issues, use 1.2.0+ + +# Credits +* ShadersMod + * Original ShadersMods: karyonix, sonic ether, id_miner, daxnitro + * eigenraven, Caedis, glowredman, and mitchej123 for the conversion of ASM to Mixin + * eigenraven and vlaetansky for getting the original ShadersMod compiling again +* ArchaicFix + * Includes graphic enhancements from [ArchaicFix](https://github.com/embeddedt/ArchaicFix/tree/4afb943751883b27a1f0aa7c5ba71698bc897e95) by embeddedt under the [LGPL](https://github.com/embeddedt/ArchaicFix/blob/main/LICENSE.md) +* NotFine + * Includes modified version of [NotFine](https://github.com/jss2a98aj/NotFine/tree/a2652e24b5654f437fb7138f6d1f5e947a7d7125) by jss2a98aj under the LGPL +* Iris Shaders + * The [Iris](https://github.com/IrisShaders/Iris) Team: coderbot, IMS212, Justsnoopy30, FoundationGames + * Asek3 for the Iris Forge port [Oculus](https://github.com/Asek3/Oculus/tree/839ce8eca8cf0c4f6b7a1322b906e5c99125ca69) + * Backported code under the LGPL +* Sodium + * JellySquid & the CaffeineMC team, for making Sodium in the first place + * Embeddedt for [Embeddium](https://github.com/embeddedt/embeddium/tree/dc59ca357c25beefd6288f0d1d40b4cd8e670ab8) under the [LGPL 3.0](https://github.com/embeddedt/embeddium/blob/16.x/forge/LICENSE) +* Neodymium + * Makamys for [Neodymium](https://github.com/makamys/Neodymium) - Various Tessellator -> Quad functionality taken under the LGPL 3 +* HUDCaching + * Created by [Moulberry](https://github.com/Moulberry/MCHUDCaching), backported from [Patcher](https://github.com/Sk1erLLC/Patcher) mod by Alexdoru under the CC-BY-SA-NC 4.0 license +* Angelica + * mitchej123, Omni, Makamys, Embeddedt, NanoLive, Caedis, Cardinalstar, Alexdoru, Eigenraven, mist475, Clepto for helping get Angelica up and running! + * IMS, jss2a98aj, || dbp ||, roadhog360 for advice and moral support diff --git a/addon.gradle b/addon.gradle index 8535d4509..58cadb25b 100644 --- a/addon.gradle +++ b/addon.gradle @@ -1,11 +1,7 @@ - -configurations { - compileStubs -} - -sourceSets { - compileStubsStubs - compileStubs { - compileClasspath += configurations.compileStubs +allprojects { + gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xmaxerrs" << "2000" + } } } diff --git a/build.gradle b/build.gradle index b894d6477..5eab54f53 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -//version: 1692122114 +//version: 1699290261 /* DO NOT CHANGE THIS FILE! Also, you may replace this file at any time if there is an update available. @@ -89,6 +89,23 @@ def out = services.get(StyledTextOutputFactory).create('an-output') def projectJavaVersion = JavaLanguageVersion.of(8) boolean disableSpotless = project.hasProperty("disableSpotless") ? project.disableSpotless.toBoolean() : false +boolean disableCheckstyle = project.hasProperty("disableCheckstyle") ? project.disableCheckstyle.toBoolean() : false + +final String CHECKSTYLE_CONFIG = """ + + + + + + + + + + + +""" checkPropertyExists("modName") checkPropertyExists("modId") @@ -140,6 +157,17 @@ if (!disableSpotless) { apply from: Blowdryer.file('spotless.gradle') } +if (!disableCheckstyle) { + apply plugin: 'checkstyle' + tasks.named("checkstylePatchedMc") { enabled = false } + tasks.named("checkstyleMcLauncher") { enabled = false } + tasks.named("checkstyleIdeVirtualMain") { enabled = false } + tasks.named("checkstyleInjectedTags") { enabled = false } + checkstyle { + config = resources.text.fromString(CHECKSTYLE_CONFIG) + } +} + String javaSourceDir = "src/main/java/" String scalaSourceDir = "src/main/scala/" String kotlinSourceDir = "src/main/kotlin/" @@ -274,7 +302,7 @@ if (apiPackage) { } if (accessTransformersFile) { - for (atFile in accessTransformersFile.split(",")) { + for (atFile in accessTransformersFile.split(" ")) { String targetFile = "src/main/resources/META-INF/" + atFile.trim() if (!getFile(targetFile).exists()) { throw new GradleException("Could not resolve \"accessTransformersFile\"! Could not find " + targetFile) @@ -600,15 +628,10 @@ repositories { } maven { name = "ic2" - url = "https://maven.ic2.player.to/" - metadataSources { - mavenPom() - artifact() + url = getURL("https://maven2.ic2.player.to/", "https://maven.ic2.player.to/") + content { + includeGroup "net.industrial-craft" } - } - maven { - name = "ic2-mirror" - url = "https://maven2.ic2.player.to/" metadataSources { mavenPom() artifact() @@ -623,7 +646,7 @@ repositories { def mixinProviderGroup = "io.github.legacymoddingmc" def mixinProviderModule = "unimixins" -def mixinProviderVersion = "0.1.7.1" +def mixinProviderVersion = "0.1.13" def mixinProviderSpecNoClassifer = "${mixinProviderGroup}:${mixinProviderModule}:${mixinProviderVersion}" def mixinProviderSpec = "${mixinProviderSpecNoClassifer}:dev" ext.mixinProviderSpec = mixinProviderSpec @@ -770,23 +793,14 @@ ext.java17PatchDependenciesCfg = configurations.create("java17PatchDependencies" } dependencies { - def lwjgl3ifyVersion = '1.4.0' - def asmVersion = '9.4' + def lwjgl3ifyVersion = '1.5.4' if (modId != 'lwjgl3ify') { java17Dependencies("com.github.GTNewHorizons:lwjgl3ify:${lwjgl3ifyVersion}") } if (modId != 'hodgepodge') { - java17Dependencies('com.github.GTNewHorizons:Hodgepodge:2.2.26') + java17Dependencies('com.github.GTNewHorizons:Hodgepodge:2.4.3') } - java17PatchDependencies('net.minecraft:launchwrapper:1.17.2') {transitive = false} - java17PatchDependencies("org.ow2.asm:asm:${asmVersion}") - java17PatchDependencies("org.ow2.asm:asm-commons:${asmVersion}") - java17PatchDependencies("org.ow2.asm:asm-tree:${asmVersion}") - java17PatchDependencies("org.ow2.asm:asm-analysis:${asmVersion}") - java17PatchDependencies("org.ow2.asm:asm-util:${asmVersion}") - java17PatchDependencies('org.ow2.asm:asm-deprecated:7.1') - java17PatchDependencies("org.apache.commons:commons-lang3:3.12.0") java17PatchDependencies("com.github.GTNewHorizons:lwjgl3ify:${lwjgl3ifyVersion}:forgePatches") {transitive = false} } @@ -958,10 +972,6 @@ if (usesShadowedDependencies.toBoolean()) { configurations.apiElements.outgoing.artifacts.clear() configurations.runtimeElements.outgoing.artifact(tasks.named("shadowJar", ShadowJar)) configurations.apiElements.outgoing.artifact(tasks.named("shadowJar", ShadowJar)) - tasks.named("jar", Jar) { - enabled = false - finalizedBy(tasks.shadowJar) - } tasks.named("reobfJar", ReobfuscatedJar) { inputJar.set(tasks.named("shadowJar", ShadowJar).flatMap({it.archiveFile})) } @@ -969,11 +979,6 @@ if (usesShadowedDependencies.toBoolean()) { javaComponent.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { skip() } - for (runTask in ["runClient", "runServer", "runClient17", "runServer17"]) { - tasks.named(runTask).configure { - dependsOn("shadowJar") - } - } } ext.publishableDevJar = usesShadowedDependencies.toBoolean() ? tasks.shadowJar : tasks.jar ext.publishableObfJar = tasks.reobfJar @@ -1173,9 +1178,8 @@ publishing { version = System.getenv("RELEASE_VERSION") ?: identifiedVersion } } - repositories { - if (usesMavenPublishing.toBoolean()) { + if (usesMavenPublishing.toBoolean() && System.getenv("MAVEN_USER") != null) { maven { url = mavenPublishUrl allowInsecureProtocol = mavenPublishUrl.startsWith("http://") // Mostly for the GTNH maven @@ -1576,6 +1580,25 @@ def getSecondaryArtifacts() { return secondaryArtifacts } +def getURL(String main, String fallback) { + return pingURL(main, 10000) ? main : fallback +} + +// credit: https://stackoverflow.com/a/3584332 +def pingURL(String url, int timeout) { + url = url.replaceFirst("^https", "http") // Otherwise an exception may be thrown on invalid SSL certificates. + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection() + connection.setConnectTimeout(timeout) + connection.setReadTimeout(timeout) + connection.setRequestMethod("HEAD") + int responseCode = connection.getResponseCode() + return 200 <= responseCode && responseCode <= 399 + } catch (IOException ignored) { + return false + } +} + // For easier scripting of things that require variables defined earlier in the buildscript if (file('addon.late.gradle.kts').exists()) { apply from: 'addon.late.gradle.kts' diff --git a/dependencies.gradle b/dependencies.gradle index 6478ae269..cc3bc52d4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,10 +1,77 @@ +/* + * Add your dependencies here. Supported configurations: + * - api("group:name:version:classifier"): if you use the types from this dependency in the public API of this mod + * Available at runtime and compiletime for mods depending on this mod + * - implementation("g:n:v:c"): if you need this for internal implementation details of the mod, but none of it is visible via the public API + * Available at runtime but not compiletime for mods depending on this mod + * - compileOnly("g:n:v:c"): if the mod you're building doesn't need this dependency during runtime at all, e.g. for optional mods + * Not available at all for mods depending on this mod, only visible at compiletime for this mod + * - compileOnlyApi("g:n:v:c"): like compileOnly, but also visible at compiletime for mods depending on this mod + * Available at compiletime but not runtime for mods depending on this mod + * - runtimeOnlyNonPublishable("g:n:v:c"): if you want to include a mod in this mod's runClient/runServer runs, but not publish it as a dependency + * Not available at all for mods depending on this mod, only visible at runtime for this mod + * - devOnlyNonPublishable("g:n:v:c"): a combination of runtimeOnlyNonPublishable and compileOnly for dependencies present at both compiletime and runtime, + * but not published as Maven dependencies - useful for RFG-deobfuscated dependencies or local testing + * - runtimeOnly("g:n:v:c"): if you don't need this at compile time, but want it to be present at runtime + * Available at runtime for mods depending on this mod + * - annotationProcessor("g:n:v:c"): mostly for java compiler plugins, if you know you need this, use it, otherwise don't worry + * - testCONFIG("g:n:v:c") - replace CONFIG by one of the above (except api), same as above but for the test sources instead of main + * + * - shadowImplementation("g:n:v:c"): effectively the same as API, but the dependency is included in your jar under a renamed package name + * Requires you to enable usesShadowedDependencies in gradle.properties + * + * - compile("g:n:v:c"): deprecated, replace with "api" (works like the old "compile") or "implementation" (can be more efficient) + * + * You can exclude transitive dependencies (dependencies of the chosen dependency) by appending { transitive = false } if needed, + * but use this sparingly as it can break using your mod as another mod's dependency if you're not careful. + * + * To depend on obfuscated jars you can use `devOnlyNonPublishable(rfg.deobf("dep:spec:1.2.3"))` to fetch an obfuscated jar from maven, + * or `devOnlyNonPublishable(rfg.deobf(project.files("libs/my-mod-jar.jar")))` to use a file. + * + * Gradle names for some of the configuration can be misleading, compileOnlyApi and runtimeOnly both get published as dependencies in Maven, but compileOnly does not. + * The buildscript adds runtimeOnlyNonPublishable to also have a runtime dependency that's not published. + * + * For more details, see https://docs.gradle.org/8.0.1/userguide/java_library_plugin.html#sec:java_library_configurations_graph + */ dependencies { - compileOnly(sourceSets.compileStubs.output) - compileStubs(sourceSets.compileStubsStubs.output) - - compileOnly("com.github.GTNewHorizons:ProjectRed:4.7.12-GTNH:dev") - compileOnly(files("dependencies/SmartRender-1.7.10-2.1-dev.jar")) + compileOnly("com.github.GTNewHorizons:Hodgepodge:2.4.3") + + compileOnly("org.projectlombok:lombok:1.18.22") {transitive = false } + annotationProcessor("org.projectlombok:lombok:1.18.22") + + // Iris Shaders + compileOnly('org.jetbrains:annotations:24.0.1') + shadowImplementation("it.unimi.dsi:fastutil:8.2.1") // Apache 2.0 + shadowImplementation("org.joml:joml:1.10.5") // MIT + shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0 + implementation("io.github.douira:glsl-transformer:1.0.0") + shadowImplementation("io.github.douira:glsl-transformer:1.0.0") { // glsl-transformer Noncommercial License 1.0.0 + exclude module: "antlr4" // we only want to shadow the runtime module + } + compileOnly "org.apache.ant:ant:1.8.2" + + // Because who doesn't want NEI + compileOnly('com.github.GTNewHorizons:NotEnoughItems:2.4.9-GTNH:dev') + compileOnly('com.github.GTNewHorizons:CodeChickenCore:1.2.0:dev') + + // Notfine Deps + compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev") + runtimeOnly("com.github.GTNewHorizons:Baubles:1.0.1.16:dev") + compileOnly("com.github.GTNewHorizons:twilightforest:2.5.1:dev") {transitive = false } + compileOnly(rfg.deobf('curse.maven:witchery-69673:2234410')) + + + compileOnly(rfg.deobf("curse.maven:extrautils-225561:2264383")) + compileOnly(rfg.deobf("curse.maven:dynamiclights-227874:2337326")) + + compileOnly("com.github.GTNewHorizons:NotEnoughIds:1.5.3:dev") // Mixin Version + compileOnly("com.github.GTNewHorizons:NotEnoughIds-Legacy:1.4.7:dev") // ASM Version + + compileOnly("com.github.GTNewHorizons:Jabba:1.3.1:dev") + // HMMMMM + compileOnly(rfg.deobf("curse.maven:journeymap-32274:2367915")) + + runtimeOnly(deobf("https://github.com/makamys/CoreTweaks/releases/download/0.3.3.2/CoreTweaks-1.7.10-0.3.3.2+nomixin.jar")) - runtimeOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.3.55-GTNH:dev") } diff --git a/gradle.properties b/gradle.properties index 229caa031..dc5aec721 100644 --- a/gradle.properties +++ b/gradle.properties @@ -62,12 +62,12 @@ apiPackage = # Specify the configuration file for Forge's access transformers here. It must be placed into /src/main/resources/META-INF/ # There can be multiple files in a comma-separated list. # Example value: mymodid_at.cfg,nei_at.cfg -accessTransformersFile = angelica_at.cfg +accessTransformersFile = angelica_at.cfg notfine_at.cfg archaicfix_at.cfg # Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! usesMixins = true # Adds some debug arguments like verbose output and export -usesMixinDebug = true +usesMixinDebug = false # Specify the location of your implementation of IMixinConfigPlugin. Leave it empty otherwise. mixinPlugin = # Specify the package that contains all of your Mixins. You may only place Mixins in this package or the build will fail! @@ -85,7 +85,7 @@ forceEnableMixins = false # If enabled, you may use 'shadowCompile' for dependencies. They will be integrated in your jar. It is your # responsibility check the licence and request permission for distribution, if required. -usesShadowedDependencies = false +usesShadowedDependencies = true # If disabled, won't remove unused classes from shaded dependencies. Some libraries use reflection to access # their own classes, making the minimization unreliable. minimizeShadowedDependencies = true @@ -136,7 +136,8 @@ curseForgeRelations = # Uncomment this to disable spotless checks # This should only be uncommented to keep it easier to sync with upstream/other forks. # That is, if there is no other active fork/upstream, NEVER change this. -# disableSpotless = true +# Disabling to make easier to sync if some of the additional mods get any changes upstream +disableSpotless = true # Override the IDEA build type. Valid value is "" (leave blank, do not override), "idea" (force use native IDEA build), "gradle" # (force use delegated build). diff --git a/repositories.gradle b/repositories.gradle index c8843905d..ce179e75b 100644 --- a/repositories.gradle +++ b/repositories.gradle @@ -1,5 +1,13 @@ // Add any additional repositories for your dependencies here repositories { - + maven { + name = 'covers1624 maven' + url = 'https://nexus.covers1624.net/repository/maven-hosted/' + metadataSources { + mavenPom() + artifact() + } + } + mavenLocal() } diff --git a/src/compileStubs/java/net/minecraft/client/renderer/texture/AbstractTexture.java b/src/compileStubs/java/net/minecraft/client/renderer/texture/AbstractTexture.java deleted file mode 100644 index afd7ea1d3..000000000 --- a/src/compileStubs/java/net/minecraft/client/renderer/texture/AbstractTexture.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.minecraft.client.renderer.texture; - -import com.gtnewhorizons.angelica.client.MultiTexID; - -public abstract class AbstractTexture implements ITextureObject { - - public int glTextureId = -1; - public MultiTexID angelica$multiTex; - - public int getGlTextureId() { - return 0; - } - - public void deleteGlTexture() {} - - public MultiTexID angelica$getMultiTexID() { - return null; - } -} diff --git a/src/compileStubs/java/net/minecraft/client/renderer/texture/ITextureObject.java b/src/compileStubs/java/net/minecraft/client/renderer/texture/ITextureObject.java deleted file mode 100644 index 3749903ec..000000000 --- a/src/compileStubs/java/net/minecraft/client/renderer/texture/ITextureObject.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.minecraft.client.renderer.texture; - -import com.gtnewhorizons.angelica.client.MultiTexID; - -public interface ITextureObject { - - MultiTexID angelica$getMultiTexID(); - - int getGlTextureId(); -} diff --git a/src/compileStubs/java/net/minecraft/client/renderer/texture/TextureMap.java b/src/compileStubs/java/net/minecraft/client/renderer/texture/TextureMap.java deleted file mode 100644 index b8e2a707c..000000000 --- a/src/compileStubs/java/net/minecraft/client/renderer/texture/TextureMap.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.minecraft.client.renderer.texture; - -import net.minecraft.client.resources.IResourceManager; -import net.minecraft.util.ResourceLocation; - -public class TextureMap extends AbstractTexture { - - public static ResourceLocation locationBlocksTexture; - - public int angelica$atlasWidth; - public int angelica$atlasHeight; - - public void loadTextureAtlas(IResourceManager p_110571_1_) {} - - public ResourceLocation completeResourceLocation(ResourceLocation p_147634_1_, int p_147634_2_) { - return null; - } - - public void updateAnimations() {} - - public int getTextureType() { - return 0; - } -} diff --git a/src/compileStubsStubs/README.MD b/src/compileStubsStubs/README.MD deleted file mode 100644 index 41a7b63cf..000000000 --- a/src/compileStubsStubs/README.MD +++ /dev/null @@ -1 +0,0 @@ -Stubs for the compile stubs diff --git a/src/compileStubsStubs/java/com/gtnewhorizons/angelica/client/MultiTexID.java b/src/compileStubsStubs/java/com/gtnewhorizons/angelica/client/MultiTexID.java deleted file mode 100644 index c64dbcb5e..000000000 --- a/src/compileStubsStubs/java/com/gtnewhorizons/angelica/client/MultiTexID.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -public class MultiTexID { -} diff --git a/src/compileStubsStubs/java/net/minecraft/client/resources/IResourceManager.java b/src/compileStubsStubs/java/net/minecraft/client/resources/IResourceManager.java deleted file mode 100644 index 49577ca30..000000000 --- a/src/compileStubsStubs/java/net/minecraft/client/resources/IResourceManager.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.minecraft.client.resources; - -public interface IResourceManager { - -} diff --git a/src/compileStubsStubs/java/net/minecraft/util/ResourceLocation.java b/src/compileStubsStubs/java/net/minecraft/util/ResourceLocation.java deleted file mode 100644 index bb0f42125..000000000 --- a/src/compileStubsStubs/java/net/minecraft/util/ResourceLocation.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.minecraft.util; - -public class ResourceLocation { - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/AngelicaLateMixins.java b/src/main/java/com/gtnewhorizons/angelica/AngelicaLateMixins.java new file mode 100644 index 000000000..5635a12c7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/AngelicaLateMixins.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica; + +import com.gtnewhorizon.gtnhmixins.ILateMixinLoader; +import com.gtnewhorizon.gtnhmixins.LateMixin; +import com.gtnewhorizons.angelica.mixins.Mixins; + +import java.util.List; +import java.util.Set; + +@LateMixin +public class AngelicaLateMixins implements ILateMixinLoader { + @Override + public String getMixinConfig() { + return "mixins.angelica.late.json"; + } + + @Override + public List getMixins(Set loadedMods) { + return Mixins.getLateMixins(loadedMods); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java index c15af07a2..91d120bca 100644 --- a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java +++ b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java @@ -1,12 +1,47 @@ package com.gtnewhorizons.angelica; +import com.gtnewhorizons.angelica.proxy.CommonProxy; +import com.gtnewhorizons.angelica.utils.AnimationMode; +import com.gtnewhorizons.angelica.utils.ManagedEnum; +import cpw.mods.fml.common.Loader; import cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; @Mod( modid = "angelica", name = "Angelica", version = Tags.VERSION, + dependencies = " before:lwjgl3ify@[1.5.3,);" + " after:hodgepodge@[2.4.3,);" + " after:CodeChickenCore@[1.2.0,);", acceptedMinecraftVersions = "[1.7.10]", acceptableRemoteVersions = "*") public class AngelicaMod { + @SidedProxy(clientSide = "com.gtnewhorizons.angelica.proxy.ClientProxy", serverSide = "com.gtnewhorizons.angelica.proxy.CommonProxy") + public static CommonProxy proxy; + /** Mixin Version */ + public static boolean isNEIDLoaded; + /** ASM Version */ + public static boolean isOldNEIDLoaded; + public static final boolean lwjglDebug = Boolean.parseBoolean(System.getProperty("org.lwjgl.util.Debug", "false")); + + public static final ManagedEnum animationsMode = new ManagedEnum<>(AnimationMode.VISIBLE_ONLY); + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + isNEIDLoaded = Loader.isModLoaded("neid"); + isOldNEIDLoaded = Loader.isModLoaded("notenoughIDs"); + proxy.preInit(event); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + proxy.init(event); + } + + @Mod.EventHandler + public void postInit(FMLPostInitializationEvent event) { + proxy.postInit(event); + } } diff --git a/src/main/java/com/gtnewhorizons/angelica/TODO.md b/src/main/java/com/gtnewhorizons/angelica/TODO.md new file mode 100644 index 000000000..3c9dd3529 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/TODO.md @@ -0,0 +1,10 @@ +- [x] Frustum (Seems related to Frustrum) +- [X] NativeImage +- [ ] PoseStack +- [ ] RenderType +- [ ] MultiBufferSource.BufferSource +- [ ] VertexBuffer +- [ ] VertexConsumer +- [ ] VertextFormat +- [ ] TextureAtlas (Seems related to TextureManager) - PBR related +- [ ] GUI diff --git a/src/main/java/com/gtnewhorizons/angelica/client/DefaultTexture.java b/src/main/java/com/gtnewhorizons/angelica/client/DefaultTexture.java deleted file mode 100644 index 3dc99cdfc..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/DefaultTexture.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import net.minecraft.client.renderer.texture.AbstractTexture; -import net.minecraft.client.resources.IResourceManager; - -public class DefaultTexture extends AbstractTexture { - - public DefaultTexture() { - loadTexture(null); - } - - public void loadTexture(IResourceManager resourcemanager) { - int[] aint = ShadersTex.createAIntImage(1, 0xFFFFFFFF); - ShadersTex.setupTexture(this.angelica$getMultiTexID(), aint, 1, 1, false, false); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/GuiShaders.java b/src/main/java/com/gtnewhorizons/angelica/client/GuiShaders.java deleted file mode 100644 index f707b25b8..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/GuiShaders.java +++ /dev/null @@ -1,366 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.util.List; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.settings.GameSettings; - -import org.lwjgl.Sys; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -public class GuiShaders extends GuiScreen { - - /** This GUI's parent GUI. */ - protected GuiScreen parentGui; - - private int updateTimer = -1; - public boolean needReinit; - - /* - * private class GuiListShaderpacks { int posX,posY,sizeX,sizeY; int viewPosY; int itemHeight = 20; int selection; - * ArrayList listShaderpacks; public void draw() { Tessellator tess = Tessellator.instance; int listSize = - * listShaderpacks.size(); int i,j; for (i=viewPosY/itemHeight, j=Math.min((viewPosY+sizeY)/itemHeight,listSize); - * i buttonList = this.buttonList; - int width = this.width; - int height = this.height; - buttonList.add( - new GuiButton( - 17, - width * 3 / 4 - 60, - 30, - 160, - 18, - "NormalMap: " + toStringOnOff(Shaders.configNormalMap))); - buttonList.add( - new GuiButton( - 18, - width * 3 / 4 - 60, - 50, - 160, - 18, - "SpecularMap: " + toStringOnOff(Shaders.configSpecularMap))); - buttonList.add( - new GuiButton( - 15, - width * 3 / 4 - 60, - 70, - 160, - 18, - "RenderResMul: " + String.format("%.04f", Shaders.configRenderResMul))); - buttonList.add( - new GuiButton( - 16, - width * 3 / 4 - 60, - 90, - 160, - 18, - "ShadowResMul: " + String.format("%.04f", Shaders.configShadowResMul))); - buttonList.add( - new GuiButton( - 10, - width * 3 / 4 - 60, - 110, - 160, - 18, - "HandDepth: " + String.format("%.04f", Shaders.configHandDepthMul))); - buttonList.add( - new GuiButton( - 9, - width * 3 / 4 - 60, - 130, - 160, - 18, - "CloudShadow: " + toStringOnOff(Shaders.configCloudShadow))); - // buttonList.add(new GuiButton(14, width *3 /4 -60, 150, 160, 18, "ShadowClipFrustrum: " - // +toStringOnOff(Shaders.configShadowClipFrustrum))); - buttonList.add( - new GuiButton( - 4, - width * 3 / 4 - 60, - 170, - 160, - 18, - "tweakBlockDamage: " + toStringOnOff(Shaders.configTweakBlockDamage))); - buttonList.add( - new GuiButton( - 19, - width * 3 / 4 - 60, - 190, - 160, - 18, - "OldLighting: " + toStringOnOff(Shaders.configOldLighting))); - // buttonList.add(new GuiButton(11, width *3 /4 -60, 210, 160, 18, "Tex Min: " - // +Shaders.texMinFilDesc[Shaders.configTexMinFilB])); - // buttonList.add(new GuiButton(12, width *3 /4 -60, 230, 160, 18, "Tex_n Mag: " - // +Shaders.texMagFilDesc[Shaders.configTexMagFilN])); - // buttonList.add(new GuiButton(13, width *3 /4 -60, 250, 160, 18, "Tex_s Mag: " - // +Shaders.texMagFilDesc[Shaders.configTexMagFilS])); - buttonList.add(new GuiButton(6, width * 3 / 4 - 60, height - 25, 160, 20, "Done")); - buttonList.add(new GuiButton(5, width / 4 - 80, height - 25, 160, 20, "Open shaderpacks folder")); - this.shaderList = new GuiSlotShaders(this); - this.shaderList.registerScrollButtons(7, 8); // registerScrollButtons(7, 8); - this.needReinit = false; - } - - @Override - /** - * actionPerformed Fired when a control is clicked. This is the equivalent of - * ActionListener.actionPerformed(ActionEvent e). - */ - protected void actionPerformed(GuiButton par1GuiButton) { - if (par1GuiButton.enabled) // enabled - { - switch (par1GuiButton.id) // id - { - case 4: /* New block breaking */ - Shaders.configTweakBlockDamage = !Shaders.configTweakBlockDamage; - // displayString - par1GuiButton.displayString = "tweakBlockDamage: " + toStringOnOff(Shaders.configTweakBlockDamage); - break; - - case 9: /* Cloud shadow */ - Shaders.configCloudShadow = !Shaders.configCloudShadow; - par1GuiButton.displayString = "CloudShadow: " + toStringOnOff(Shaders.configCloudShadow); - break; - - case 10: /* Hand Depth */ { - float val = Shaders.configHandDepthMul; - float[] choices = { 0.0625f, 0.125f, 0.25f, 0.5f, 1.0f }; - int i; - if (!isShiftKeyDown()) { // isShiftKeyDown - for (i = 0; i < choices.length && choices[i] <= val; ++i) {} - if (i == choices.length) i = 0; - } else { - for (i = choices.length - 1; i >= 0 && val <= choices[i]; --i) {} - if (i < 0) i = choices.length - 1; - } - Shaders.configHandDepthMul = choices[i]; - par1GuiButton.displayString = "HandDepth: " + String.format("%.4f", Shaders.configHandDepthMul); - break; - } - - case 15: /* Render Resolution Multiplier */ { - float val = Shaders.configRenderResMul; - float[] choices = { 0.25f, 0.3333333333f, 0.5f, 0.7071067812f, 1.0f, 1.414213562f, 2.0f }; - int i; - if (!isShiftKeyDown()) { - for (i = 0; i < choices.length && choices[i] <= val; ++i) {} - if (i == choices.length) i = 0; - } else { - for (i = choices.length - 1; i >= 0 && val <= choices[i]; --i) {} - if (i < 0) i = choices.length - 1; - } - Shaders.configRenderResMul = choices[i]; - par1GuiButton.displayString = "RenderResMul: " + String.format("%.4f", Shaders.configRenderResMul); - Shaders.scheduleResize(); - break; - } - - case 16: /* Shadow Resolution Multiplier */ { - float val = Shaders.configShadowResMul; - float[] choices = { 0.25f, 0.3333333333f, 0.5f, 0.7071067812f, 1.0f, 1.414213562f, 2.0f, 3.0f, - 4.0f }; - int i; - if (!isShiftKeyDown()) { - for (i = 0; i < choices.length && choices[i] <= val; ++i) {} - if (i == choices.length) i = 0; - } else { - for (i = choices.length - 1; i >= 0 && val <= choices[i]; --i) {} - if (i < 0) i = choices.length - 1; - } - Shaders.configShadowResMul = choices[i]; - par1GuiButton.displayString = "ShadowResMul: " + String.format("%.4f", Shaders.configShadowResMul); - Shaders.scheduleResizeShadow(); - break; - } - - case 17: /* Normal Map */ { - Shaders.configNormalMap = !Shaders.configNormalMap; - // displayString - par1GuiButton.displayString = "NormapMap: " + toStringOnOff(Shaders.configNormalMap); - mc.scheduleResourcesRefresh(); // schedule refresh texture - break; - } - - case 18: /* Normal Map */ { - Shaders.configSpecularMap = !Shaders.configSpecularMap; - // displayString - par1GuiButton.displayString = "SpecularMap: " + toStringOnOff(Shaders.configSpecularMap); - mc.scheduleResourcesRefresh(); // schedule refresh texture - break; - } - - case 19: /* old Lighting */ { - Shaders.configOldLighting = !Shaders.configOldLighting; - // displayString - par1GuiButton.displayString = "OldLighting: " + toStringOnOff(Shaders.configOldLighting); - Shaders.updateBlockLightLevel(); - mc.renderGlobal.loadRenderers(); - break; - } - - case 11: /* texture filter */ { - Shaders.configTexMinFilB = (Shaders.configTexMinFilB + 1) % Shaders.texMinFilRange; - Shaders.configTexMinFilN = Shaders.configTexMinFilS = Shaders.configTexMinFilB; - par1GuiButton.displayString = "Tex Min: " + Shaders.texMinFilDesc[Shaders.configTexMinFilB]; - ShadersTex.updateTextureMinMagFilter(); - break; - } - - case 12: /* texture filter */ { - Shaders.configTexMagFilN = (Shaders.configTexMagFilN + 1) % Shaders.texMagFilRange; - par1GuiButton.displayString = "Tex_n Mag: " + Shaders.texMagFilDesc[Shaders.configTexMagFilN]; - ShadersTex.updateTextureMinMagFilter(); - break; - } - - case 13: /* texture filter */ { - Shaders.configTexMagFilS = (Shaders.configTexMagFilS + 1) % Shaders.texMagFilRange; - par1GuiButton.displayString = "Tex_s Mag: " + Shaders.texMagFilDesc[Shaders.configTexMagFilS]; - ShadersTex.updateTextureMinMagFilter(); - break; - } - - case 14: /* shadow frustum clipping */ { - Shaders.configShadowClipFrustrum = !Shaders.configShadowClipFrustrum; - par1GuiButton.displayString = "ShadowClipFrustrum: " - + toStringOnOff(Shaders.configShadowClipFrustrum); - ShadersTex.updateTextureMinMagFilter(); - break; - } - - case 5: /* Open shaderpacks folder */ - switch (net.minecraft.util.Util.getOSType()) { - case OSX: { - try { - Runtime.getRuntime().exec( - new String[] { "/usr/bin/open", Shaders.shaderpacksdir.getAbsolutePath() }); - return; - } catch (IOException var7) { - var7.printStackTrace(); - } - } - break; - case WINDOWS: { - String var2 = String.format( - "cmd.exe /C start \"Open file\" \"%s\"", - new Object[] { Shaders.shaderpacksdir.getAbsolutePath() }); - - try { - Runtime.getRuntime().exec(var2); - return; - } catch (IOException var6) { - var6.printStackTrace(); - } - } - break; - default: - break; - } - boolean var8 = false; - - try { - Class var3 = Class.forName("java.awt.Desktop"); - Object var4 = var3.getMethod("getDesktop", new Class[0]).invoke((Object) null, new Object[0]); - var3.getMethod("browse", new Class[] { URI.class }).invoke( - var4, - new Object[] { (new File(mc.mcDataDir, Shaders.shaderpacksdirname)).toURI() }); - } catch (Throwable var5) { - var5.printStackTrace(); - var8 = true; - } - - if (var8) { - AngelicaTweaker.LOGGER.debug("Opening via system class!"); - Sys.openURL("file://" + Shaders.shaderpacksdir.getAbsolutePath()); - } - break; - - case 6: /* Done */ - try { - Shaders.storeConfig(); - } catch (Exception ex) {} - if (needReinit) { - needReinit = false; - Shaders.loadShaderPack(); - Shaders.uninit(); - this.mc.renderGlobal.loadRenderers(); - } - this.mc.displayGuiScreen(this.parentGui); // displayGuiScreen - break; - - default: - this.shaderList.actionPerformed(par1GuiButton); // actionPerformed - } - } - } - - @Override - /** - * Draws the screen and all the components in it. - */ - public void drawScreen(int par1, int par2, float par3) { - drawDefaultBackground(); // background - this.shaderList.drawScreen(par1, par2, par3); // drawScreen - - if (this.updateTimer <= 0) { - this.shaderList.updateList(); - this.updateTimer += 20; - } - - this.drawCenteredString(this.fontRendererObj, "Shaders ", this.width / 2, 16, 0xffffff); - this.drawCenteredString(this.fontRendererObj, " v" + Shaders.versionString, this.width - 40, 10, 0x808080); - // this.drawCenteredString(this.fontRenderer, "( Place zipped Shader files here. )", this.width / 4 + 10, - // this.height - 26, 0x808080); - super.drawScreen(par1, par2, par3); - } - - @Override - /** - * Called from the main game loop to update the screen. - */ - public void updateScreen() { - super.updateScreen(); - --this.updateTimer; - } - - public Minecraft getMc() { - return this.mc; - } - - public void drawCenteredString(String par1, int par2, int par3, int par4) { - this.drawCenteredString(this.fontRendererObj, par1, par2, par3, par4); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/GuiSlotShaders.java b/src/main/java/com/gtnewhorizons/angelica/client/GuiSlotShaders.java deleted file mode 100644 index 8f3c33a16..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/GuiSlotShaders.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.util.List; - -import net.minecraft.client.gui.GuiSlot; -import net.minecraft.client.renderer.Tessellator; - -class GuiSlotShaders extends GuiSlot { - - private List shaderslist; - - final GuiShaders shadersGui; - - public GuiSlotShaders(GuiShaders par1GuiShaders) { - // super(par1GuiShaders.getMc(), par1GuiShaders.width / 2 + 20, par1GuiShaders.height, 40, par1GuiShaders.height - // - 70, 16); - super( - par1GuiShaders.getMc(), - par1GuiShaders.width / 2 + 20, - par1GuiShaders.height, - 40, - par1GuiShaders.height - 70, - 16); - this.shadersGui = par1GuiShaders; - this.shaderslist = Shaders.listofShaders(); - } - - public void updateList() { - this.shaderslist = Shaders.listofShaders(); - } - - @Override - /** getSize */ - protected int getSize() { - return this.shaderslist.size(); - } - - @Override - /** elementClicked */ - protected void elementClicked(int par1, boolean par2, int par3, int par4) { - Shaders.setShaderPack((String) shaderslist.get(par1)); - shadersGui.needReinit = false; - Shaders.loadShaderPack(); - Shaders.uninit(); - } - - @Override - /** isSelected */ - protected boolean isSelected(int par1) { - return ((String) this.shaderslist.get(par1)).equals(Shaders.currentshadername); - } - - @Override - /** getScrollBarX */ - protected int getScrollBarX() { - return this.width - 6; - } - - @Override - /** getContentHeight */ - protected int getContentHeight() { - return this.getSize() * 18; - } - - @Override - /** drawBackground */ - protected void drawBackground() { - // this.shadersGui.drawDefaultBackground(); - } - - @Override - /** drawSlot */ - protected void drawSlot(int par1, int par2, int par3, int par4, Tessellator par5, int par6, int par7) { - this.shadersGui.drawCenteredString( - (String) this.shaderslist.get(par1), - this.shadersGui.width / 4 + 10, - par3 + 1, - 0xffffff); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/HFNoiseTexture.java b/src/main/java/com/gtnewhorizons/angelica/client/HFNoiseTexture.java deleted file mode 100644 index 7e62f955b..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/HFNoiseTexture.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.nio.ByteBuffer; - -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -/** - * - * @author Nathanael Lane - */ -public class HFNoiseTexture { - - public int texID; - public int textureUnit; - - public HFNoiseTexture(int width, int height) { - texID = GL11.glGenTextures(); - textureUnit = 15; - - byte[] image = genHFNoiseImage(width, height); - ByteBuffer data = BufferUtils.createByteBuffer(image.length); - data.put(image); - data.flip(); - - // GL13.glActiveTexture(GL13.GL_TEXTURE0 + this.textureUnit); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texID); - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - 0, - GL11.GL_RGB, - width, - height, - 0, - GL11.GL_RGB, - GL11.GL_UNSIGNED_BYTE, - data); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - // GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - } - - public int getID() { - return texID; - } - - public void destroy() { - GL11.glDeleteTextures(texID); - texID = 0; - } - - // from George Marsaglia's paper on XORshift PRNGs - private int random(int seed) { - seed ^= (seed << 13); - seed ^= (seed >> 17); - seed ^= (seed << 5); - return seed; - } - - private byte random(int x, int y, int z) { - int seed = (random(x) + random(y * 19)) * random(z * 23) - z; - return (byte) (random(seed) % 128); - } - - /* - * Just a random value for each pixel to get maximum frequency - */ - private byte[] genHFNoiseImage(int width, int height) { - - byte[] image = new byte[width * height * 3]; - int index = 0; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - for (int z = 1; z < 4; z++) { - image[index++] = random(x, y, z); - } - } - } - - return image; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/IShaderPack.java b/src/main/java/com/gtnewhorizons/angelica/client/IShaderPack.java deleted file mode 100644 index 3b9979e0b..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/IShaderPack.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.InputStream; - -public interface IShaderPack { - - void close(); - - InputStream getResourceAsStream(String resName); -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/MultiTexID.java b/src/main/java/com/gtnewhorizons/angelica/client/MultiTexID.java deleted file mode 100644 index 0c8e18c64..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/MultiTexID.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -public class MultiTexID { - - public int base; - public int norm; - public int spec; - - public MultiTexID(int baseTex, int normTex, int specTex) { - base = baseTex; - norm = normTex; - spec = specTex; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackDefault.java b/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackDefault.java deleted file mode 100644 index e407d4c9f..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackDefault.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.InputStream; - -public class ShaderPackDefault implements IShaderPack { - - public ShaderPackDefault() {} - - @Override - public void close() {} - - @Override - public InputStream getResourceAsStream(String resName) { - return ShaderPackDefault.class.getResourceAsStream(resName); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackFolder.java b/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackFolder.java deleted file mode 100644 index 682e2c6bd..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackFolder.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; - -public class ShaderPackFolder implements IShaderPack { - - protected File packFile; - - public ShaderPackFolder(String name, File file) { - packFile = file; - } - - @Override - public void close() {} - - @Override - public InputStream getResourceAsStream(String resName) { - try { - File resFile = new File(packFile, resName.substring(1)); - if (resFile != null) { - return new BufferedInputStream(new FileInputStream(resFile)); - } - } catch (Exception excp) {} - return null; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackNone.java b/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackNone.java deleted file mode 100644 index 552bb1606..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackNone.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.InputStream; - -public class ShaderPackNone implements IShaderPack { - - public ShaderPackNone() {} - - @Override - public void close() {} - - @Override - public InputStream getResourceAsStream(String resName) { - return null; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackZip.java b/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackZip.java deleted file mode 100644 index 8315a20e5..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShaderPackZip.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.io.File; -import java.io.InputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class ShaderPackZip implements IShaderPack { - - protected File packFile; - protected ZipFile packZipFile; - - public ShaderPackZip(String name, File file) { - packFile = file; - packZipFile = null; - } - - @Override - public void close() { - if (packZipFile != null) { - try { - packZipFile.close(); - } catch (Exception excp) {} - packZipFile = null; - } - } - - @Override - public InputStream getResourceAsStream(String resName) { - if (packZipFile == null) { - try { - packZipFile = new ZipFile(packFile); - } catch (Exception excp) {} - } - if (packZipFile != null) { - try { - ZipEntry entry = packZipFile.getEntry(resName.substring(1)); - if (entry != null) { - return packZipFile.getInputStream(entry); - } - } catch (Exception excp) {} - } - return null; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/Shaders.java b/src/main/java/com/gtnewhorizons/angelica/client/Shaders.java deleted file mode 100644 index 3728a27b1..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/Shaders.java +++ /dev/null @@ -1,3793 +0,0 @@ -// Code written by daxnitro modified by id_miner and karyonix. -// Do what you want with it but give us some credit if you use it in whole or in part. - -package com.gtnewhorizons.angelica.client; - -import static org.lwjgl.opengl.ARBFragmentShader.GL_FRAGMENT_SHADER_ARB; -import static org.lwjgl.opengl.ARBShaderObjects.GL_OBJECT_INFO_LOG_LENGTH_ARB; -import static org.lwjgl.opengl.ARBShaderObjects.glAttachObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glCompileShaderARB; -import static org.lwjgl.opengl.ARBShaderObjects.glCreateProgramObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glCreateShaderObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glDeleteObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glDetachObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glGetInfoLogARB; -import static org.lwjgl.opengl.ARBShaderObjects.glGetObjectParameterARB; -import static org.lwjgl.opengl.ARBShaderObjects.glGetUniformLocationARB; -import static org.lwjgl.opengl.ARBShaderObjects.glLinkProgramARB; -import static org.lwjgl.opengl.ARBShaderObjects.glShaderSourceARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUniform1fARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUniform1iARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUniform2iARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUniform3fARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUniformMatrix4ARB; -import static org.lwjgl.opengl.ARBShaderObjects.glUseProgramObjectARB; -import static org.lwjgl.opengl.ARBShaderObjects.glValidateProgramARB; -import static org.lwjgl.opengl.ARBVertexShader.GL_VERTEX_SHADER_ARB; -import static org.lwjgl.opengl.ARBVertexShader.glBindAttribLocationARB; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_FRAMEBUFFER_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_INVALID_FRAMEBUFFER_OPERATION_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.GL_MAX_COLOR_ATTACHMENTS_EXT; -import static org.lwjgl.opengl.EXTFramebufferObject.glBindFramebufferEXT; -import static org.lwjgl.opengl.EXTFramebufferObject.glCheckFramebufferStatusEXT; -import static org.lwjgl.opengl.EXTFramebufferObject.glDeleteFramebuffersEXT; -import static org.lwjgl.opengl.EXTFramebufferObject.glFramebufferTexture2DEXT; -import static org.lwjgl.opengl.EXTFramebufferObject.glGenFramebuffersEXT; -import static org.lwjgl.opengl.GL11.GL_ALPHA_TEST; -import static org.lwjgl.opengl.GL11.GL_ALWAYS; -import static org.lwjgl.opengl.GL11.GL_BLEND; -import static org.lwjgl.opengl.GL11.GL_CLAMP; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_COMPONENT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; -import static org.lwjgl.opengl.GL11.GL_FOG; -import static org.lwjgl.opengl.GL11.GL_LEQUAL; -import static org.lwjgl.opengl.GL11.GL_LINEAR; -import static org.lwjgl.opengl.GL11.GL_LINEAR_MIPMAP_LINEAR; -import static org.lwjgl.opengl.GL11.GL_LUMINANCE; -import static org.lwjgl.opengl.GL11.GL_MODELVIEW; -import static org.lwjgl.opengl.GL11.GL_MODELVIEW_MATRIX; -import static org.lwjgl.opengl.GL11.GL_NEAREST; -import static org.lwjgl.opengl.GL11.GL_NEAREST_MIPMAP_NEAREST; -import static org.lwjgl.opengl.GL11.GL_NONE; -import static org.lwjgl.opengl.GL11.GL_NO_ERROR; -import static org.lwjgl.opengl.GL11.GL_PROJECTION; -import static org.lwjgl.opengl.GL11.GL_PROJECTION_MATRIX; -import static org.lwjgl.opengl.GL11.GL_QUADS; -import static org.lwjgl.opengl.GL11.GL_RGB16; -import static org.lwjgl.opengl.GL11.GL_RGB8; -import static org.lwjgl.opengl.GL11.GL_RGBA; -import static org.lwjgl.opengl.GL11.GL_RGBA16; -import static org.lwjgl.opengl.GL11.GL_RGBA8; -import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; -import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER; -import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER; -import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S; -import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T; -import static org.lwjgl.opengl.GL11.GL_TRUE; -import static org.lwjgl.opengl.GL11.glBegin; -import static org.lwjgl.opengl.GL11.glBindTexture; -import static org.lwjgl.opengl.GL11.glClear; -import static org.lwjgl.opengl.GL11.glClearColor; -import static org.lwjgl.opengl.GL11.glColor4f; -import static org.lwjgl.opengl.GL11.glColorMask; -import static org.lwjgl.opengl.GL11.glCopyTexSubImage2D; -import static org.lwjgl.opengl.GL11.glDeleteTextures; -import static org.lwjgl.opengl.GL11.glDepthFunc; -import static org.lwjgl.opengl.GL11.glDepthMask; -import static org.lwjgl.opengl.GL11.glDisable; -import static org.lwjgl.opengl.GL11.glEnable; -import static org.lwjgl.opengl.GL11.glEnd; -import static org.lwjgl.opengl.GL11.glFlush; -import static org.lwjgl.opengl.GL11.glGenTextures; -import static org.lwjgl.opengl.GL11.glGetError; -import static org.lwjgl.opengl.GL11.glGetFloat; -import static org.lwjgl.opengl.GL11.glGetInteger; -import static org.lwjgl.opengl.GL11.glLoadIdentity; -import static org.lwjgl.opengl.GL11.glMatrixMode; -import static org.lwjgl.opengl.GL11.glOrtho; -import static org.lwjgl.opengl.GL11.glPopMatrix; -import static org.lwjgl.opengl.GL11.glPushMatrix; -import static org.lwjgl.opengl.GL11.glReadBuffer; -import static org.lwjgl.opengl.GL11.glReadPixels; -import static org.lwjgl.opengl.GL11.glRotatef; -import static org.lwjgl.opengl.GL11.glTexCoord2f; -import static org.lwjgl.opengl.GL11.glTexImage2D; -import static org.lwjgl.opengl.GL11.glTexParameterf; -import static org.lwjgl.opengl.GL11.glTexParameteri; -import static org.lwjgl.opengl.GL11.glTranslatef; -import static org.lwjgl.opengl.GL11.glVertex3f; -import static org.lwjgl.opengl.GL11.glViewport; -import static org.lwjgl.opengl.GL13.GL_TEXTURE0; -import static org.lwjgl.opengl.GL13.GL_TEXTURE1; -import static org.lwjgl.opengl.GL13.GL_TEXTURE11; -import static org.lwjgl.opengl.GL13.GL_TEXTURE12; -import static org.lwjgl.opengl.GL13.GL_TEXTURE13; -import static org.lwjgl.opengl.GL13.GL_TEXTURE14; -import static org.lwjgl.opengl.GL13.GL_TEXTURE2; -import static org.lwjgl.opengl.GL13.GL_TEXTURE3; -import static org.lwjgl.opengl.GL13.GL_TEXTURE4; -import static org.lwjgl.opengl.GL13.GL_TEXTURE5; -import static org.lwjgl.opengl.GL13.GL_TEXTURE6; -import static org.lwjgl.opengl.GL13.GL_TEXTURE7; -import static org.lwjgl.opengl.GL13.glActiveTexture; -import static org.lwjgl.opengl.GL14.GL_DEPTH_TEXTURE_MODE; -import static org.lwjgl.opengl.GL14.GL_TEXTURE_COMPARE_MODE; -import static org.lwjgl.opengl.GL20.GL_MAX_DRAW_BUFFERS; -import static org.lwjgl.opengl.GL20.GL_MAX_TEXTURE_IMAGE_UNITS; -import static org.lwjgl.opengl.GL20.GL_VALIDATE_STATUS; -import static org.lwjgl.opengl.GL20.glDrawBuffers; -import static org.lwjgl.opengl.GL20.glGetProgrami; -import static org.lwjgl.opengl.GL30.GL_COMPARE_REF_TO_TEXTURE; -import static org.lwjgl.opengl.GL30.GL_R16; -import static org.lwjgl.opengl.GL30.GL_R32F; -import static org.lwjgl.opengl.GL30.GL_R8; -import static org.lwjgl.opengl.GL30.GL_RG16; -import static org.lwjgl.opengl.GL30.GL_RG32F; -import static org.lwjgl.opengl.GL30.GL_RG8; -import static org.lwjgl.opengl.GL30.GL_RGB32F; -import static org.lwjgl.opengl.GL30.GL_RGBA32F; -import static org.lwjgl.opengl.GL30.glGenerateMipmap; -import static org.lwjgl.util.glu.GLU.gluErrorString; -import static org.lwjgl.util.glu.GLU.gluPerspective; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.minecraft.block.Block; -import net.minecraft.block.material.Material; -import net.minecraft.client.Minecraft; -import net.minecraft.client.model.ModelBase; -import net.minecraft.client.model.ModelRenderer; -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.RenderBlocks; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.renderer.entity.Render; -import net.minecraft.client.renderer.entity.RenderManager; -import net.minecraft.client.renderer.entity.RendererLivingEntity; -import net.minecraft.client.renderer.texture.ITextureObject; -import net.minecraft.client.settings.GameSettings; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.Vec3; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.ContextCapabilities; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.lwjgl.opengl.GLContext; - -import com.gtnewhorizons.angelica.Tags; -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; -import com.gtnewhorizons.angelica.mixins.interfaces.IModelRenderer; - -// import org.lwjgl.opengl.ARBVertexProgram; - -public class Shaders { - - public static final String versionString = Tags.VERSION; - public static final int versionNumber = 0x020312; - public static final int buildNumber = 53; - - private static Minecraft mc; - - public static boolean isInitialized = false; - private static boolean notFirstInit = false; - public static ContextCapabilities capabilities; - public static boolean hasGlGenMipmap = false; - public static int numberResetDisplayList = 0; - - private static int renderDisplayWidth = 0; - private static int renderDisplayHeight = 0; - public static int renderWidth = 0; - public static int renderHeight = 0; - - public static boolean isRenderingWorld = false; - public static boolean isRenderingSky = false; - public static boolean isCompositeRendered = false; - public static boolean isRenderingDfb = false; - public static boolean isShadowPass = false; - public static int activeTexUnit = 0; - - public static boolean isHandRendered; - public static ItemStack itemToRender; - - private static float[] sunPosition = new float[4]; - private static float[] moonPosition = new float[4]; - private static float[] upPosition = new float[4]; - - private static float[] upPosModelView = new float[] { 0.0F, 100.0F, 0.0F, 0.0F }; - private static float[] sunPosModelView = new float[] { 0.0F, 100.0F, 0.0F, 0.0F }; - private static float[] moonPosModelView = new float[] { 0.0F, -100.0F, 0.0F, 0.0F }; - private static float[] tempMat = new float[16]; - - private static float clearColorR; - private static float clearColorG; - private static float clearColorB; - - private static float skyColorR; - private static float skyColorG; - private static float skyColorB; - - private static long worldTime = 0; - private static long lastWorldTime = 0; - private static long diffWorldTime = 0; - private static float sunAngle = 0; - private static float shadowAngle = 0; - private static int moonPhase = 0; - - private static long systemTime = 0; - private static long lastSystemTime = 0; - private static long diffSystemTime = 0; - - private static int frameCounter = 0; - private static float frameTimeCounter = 0.0f; - - private static int systemTimeInt32 = 0; - - private static float rainStrength = 0.0f; - private static float wetness = 0.0f; - public static float wetnessHalfLife = 30 * 20; - public static float drynessHalfLife = 10 * 20; - public static float eyeBrightnessHalflife = 10.0f; - private static boolean usewetness = false; - private static int isEyeInWater = 0; - private static int eyeBrightness = 0; - private static float eyeBrightnessFadeX = 0; - private static float eyeBrightnessFadeY = 0; - private static float eyePosY = 0; - private static float centerDepth = 0; - private static float centerDepthSmooth = 0; - private static float centerDepthSmoothHalflife = 1.0f; - private static boolean centerDepthSmoothEnabled = false; - private static int superSamplingLevel = 1; - - private static boolean updateChunksErrorRecorded = false; - - private static boolean lightmapEnabled = false; - private static boolean fogEnabled = true; - - public static int entityAttrib = 10; - public static int midTexCoordAttrib = 11; - public static boolean useEntityAttrib = false; - public static boolean useMidTexCoordAttrib = false; - public static boolean useMultiTexCoord3Attrib = false; - public static boolean progUseEntityAttrib = false; - public static boolean progUseMidTexCoordAttrib = false; - - public static int atlasSizeX = 0, atlasSizeY = 0; - public static int uniformEntityHurt = -1; - public static int uniformEntityFlash = -1; - public static boolean useEntityHurtFlash; - - private static double[] previousCameraPosition = new double[3]; - private static double[] cameraPosition = new double[3]; - - // Shadow stuff - - // configuration - private static int shadowPassInterval = 0; - public static boolean needResizeShadow = false; - private static int shadowMapWidth = 1024; - private static int shadowMapHeight = 1024; - private static int spShadowMapWidth = 1024; - private static int spShadowMapHeight = 1024; - private static float shadowMapFOV = 90.0f; - private static float shadowMapHalfPlane = 160.0f; - private static boolean shadowMapIsOrtho = true; - - private static int shadowPassCounter = 0; - - private static int preShadowPassThirdPersonView; - - public static boolean shouldSkipDefaultShadow = false; - - private static boolean waterShadowEnabled = false; - - // Color attachment stuff - - private static final int MaxDrawBuffers = 8; - private static final int MaxColorBuffers = 8; - private static final int MaxDepthBuffers = 3; - private static final int MaxShadowColorBuffers = 8; - private static final int MaxShadowDepthBuffers = 2; - private static int usedColorBuffers = 0; - private static int usedDepthBuffers = 0; - private static int usedShadowColorBuffers = 0; - private static int usedShadowDepthBuffers = 0; - private static int usedColorAttachs = 0; - private static int usedDrawBuffers = 0; - - private static int dfb = 0; - private static int sfb = 0; - - private static int[] gbuffersFormat = new int[MaxColorBuffers]; - - // Program stuff - - public static int activeProgram = 0; - - public static final int ProgramNone = 0; - public static final int ProgramBasic = 1; - public static final int ProgramTextured = 2; - public static final int ProgramTexturedLit = 3; - public static final int ProgramSkyBasic = 4; - public static final int ProgramSkyTextured = 5; - public static final int ProgramTerrain = 6; - public static final int ProgramWater = 7; - public static final int ProgramEntities = 8; - public static final int ProgramSpiderEyes = 9; - public static final int ProgramHand = 10; - public static final int ProgramWeather = 11; - public static final int ProgramComposite = 12; - public static final int ProgramComposite1 = 13; - public static final int ProgramComposite2 = 14; - public static final int ProgramComposite3 = 15; - public static final int ProgramComposite4 = 16; - public static final int ProgramComposite5 = 17; - public static final int ProgramComposite6 = 18; - public static final int ProgramComposite7 = 19; - public static final int ProgramFinal = 20; - public static final int ProgramShadow = 21; - public static final int ProgramCount = 22; - public static final int MaxCompositePasses = 8; - - private static final String[] programNames = new String[] { "", "gbuffers_basic", "gbuffers_textured", - "gbuffers_textured_lit", "gbuffers_skybasic", "gbuffers_skytextured", "gbuffers_terrain", "gbuffers_water", - "gbuffers_entities", "gbuffers_spidereyes", "gbuffers_hand", "gbuffers_weather", "composite", "composite1", - "composite2", "composite3", "composite4", "composite5", "composite6", "composite7", "final", "shadow", }; - - private static final int[] programBackups = new int[] { ProgramNone, // none - ProgramNone, // basic - ProgramBasic, // textured - ProgramTextured, // textured/lit - ProgramBasic, // skybasic - ProgramTextured, // skytextured - ProgramTexturedLit, // terrain - ProgramTerrain, // water - ProgramTexturedLit, // entities - ProgramTextured, // spidereyes - ProgramTexturedLit, // hand - ProgramTexturedLit, // weather - ProgramNone, // composite - ProgramNone, // composite1 - ProgramNone, // composite2 - ProgramNone, // composite3 - ProgramNone, // composite4 - ProgramNone, // composite5 - ProgramNone, // composite6 - ProgramNone, // composite7 - ProgramNone, // final - ProgramNone, // shadow - }; - - private static int[] programsID = new int[ProgramCount]; - private static int[] programsRef = new int[ProgramCount]; - private static int programIDCopyDepth = 0; - - private static String[] programsDrawBufSettings = new String[ProgramCount]; - private static String newDrawBufSetting = null; - private static IntBuffer[] programsDrawBuffers = new IntBuffer[ProgramCount]; - static IntBuffer activeDrawBuffers = null; - - private static String[] programsColorAtmSettings = new String[ProgramCount]; - private static String newColorAtmSetting = null; - private static String activeColorAtmSettings = null; - - private static int[] programsCompositeMipmapSetting = new int[ProgramCount]; - private static int newCompositeMipmapSetting = 0; - private static int activeCompositeMipmapSetting = 0; - - public static Properties loadedShaders = null; - public static Properties shadersConfig = null; - - // public static TextureManager textureManager = null; - public static ITextureObject defaultTexture = null; - public static boolean normalMapEnabled = false; - public static boolean[] shadowHardwareFilteringEnabled = new boolean[MaxShadowDepthBuffers]; - public static boolean[] shadowMipmapEnabled = new boolean[MaxShadowDepthBuffers]; - public static boolean[] shadowFilterNearest = new boolean[MaxShadowDepthBuffers]; - public static boolean[] shadowColorMipmapEnabled = new boolean[MaxShadowColorBuffers]; - public static boolean[] shadowColorFilterNearest = new boolean[MaxShadowColorBuffers]; - - // config - public static boolean configTweakBlockDamage = false; - public static boolean configCloudShadow = true; - public static float configHandDepthMul = 0.125f; - public static float configRenderResMul = 1.0f; - public static float configShadowResMul = 1.0f; - public static int configTexMinFilB = 0; - public static int configTexMinFilN = 0; - public static int configTexMinFilS = 0; - public static int configTexMagFilB = 0; - public static int configTexMagFilN = 0; - public static int configTexMagFilS = 0; - public static boolean configShadowClipFrustrum = true; - public static boolean configNormalMap = true; - public static boolean configSpecularMap = true; - public static boolean configOldLighting = false; - - // related to config - public static final int texMinFilRange = 3; - public static final int texMagFilRange = 2; - public static final String[] texMinFilDesc = { "Nearest", "Nearest-Nearest", "Nearest-Linear" }; - public static final String[] texMagFilDesc = { "Nearest", "Linear" }; - public static final int[] texMinFilValue = { GL11.GL_NEAREST, GL11.GL_NEAREST_MIPMAP_NEAREST, - GL11.GL_NEAREST_MIPMAP_LINEAR }; - public static final int[] texMagFilValue = { GL11.GL_NEAREST, GL11.GL_LINEAR }; - - // shaderpack - static IShaderPack shaderPack = null; - static File currentshader; - static String currentshadername; - static String packNameNone = "(none)"; - static String packNameDefault = "(internal)"; - static String shaderpacksdirname = "shaderpacks"; - static String optionsfilename = "optionsshaders.txt"; - static File shadersdir = new File(Minecraft.getMinecraft().mcDataDir, "shaders"); - static File shaderpacksdir = new File(Minecraft.getMinecraft().mcDataDir, shaderpacksdirname); - static File configFile = new File(Minecraft.getMinecraft().mcDataDir, optionsfilename); - - public static final boolean enableShadersOption = true; - private static final boolean enableShadersDebug = true; - - public static float blockLightLevel05 = 0.5f; - public static float blockLightLevel06 = 0.6f; - public static float blockLightLevel08 = 0.8f; - - public static float aoLevel = 0.8f; - public static float blockAoLight = 1.0f - aoLevel; - - public static float sunPathRotation = 0.0f; - public static float shadowAngleInterval = 0.0f; - public static int fogMode = 0; - public static float fogColorR, fogColorG, fogColorB; - public static float shadowIntervalSize = 2.0f; - public static int terrainIconSize = 16; - public static int[] terrainTextureSize = new int[2]; - - private static HFNoiseTexture noiseTexture; - private static boolean noiseTextureEnabled = false; - private static int noiseTextureResolution = 256; - - // direct buffers - private static final int bigBufferSize = (16 * 12 + MaxColorBuffers - + MaxDepthBuffers - + MaxShadowColorBuffers - + MaxShadowDepthBuffers - + MaxDrawBuffers * 8 - + MaxDrawBuffers * ProgramCount) * 4; - private static final ByteBuffer bigBuffer = (ByteBuffer) BufferUtils.createByteBuffer(bigBufferSize).limit(0); - - private static final FloatBuffer previousProjection = nextFloatBuffer(16); - private static final FloatBuffer projection = nextFloatBuffer(16); - private static final FloatBuffer projectionInverse = nextFloatBuffer(16); - private static final FloatBuffer previousModelView = nextFloatBuffer(16); - private static final FloatBuffer modelView = nextFloatBuffer(16); - private static final FloatBuffer modelViewInverse = nextFloatBuffer(16); - private static final FloatBuffer shadowProjection = nextFloatBuffer(16); - private static final FloatBuffer shadowProjectionInverse = nextFloatBuffer(16); - private static final FloatBuffer shadowModelView = nextFloatBuffer(16); - private static final FloatBuffer shadowModelViewInverse = nextFloatBuffer(16); - private static final FloatBuffer tempMatrixDirectBuffer = nextFloatBuffer(16); - private static final FloatBuffer tempDirectFloatBuffer = nextFloatBuffer(16); - - private static final IntBuffer dfbColorTextures = nextIntBuffer(MaxColorBuffers); - private static final IntBuffer dfbDepthTextures = nextIntBuffer(MaxDepthBuffers); - private static final IntBuffer sfbColorTextures = nextIntBuffer(MaxShadowColorBuffers); - private static final IntBuffer sfbDepthTextures = nextIntBuffer(MaxShadowDepthBuffers); - - static final IntBuffer dfbDrawBuffers = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer sfbDrawBuffers = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersNone = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersAll = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersClear0 = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersClear1 = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersClearColor = nextIntBuffer(MaxDrawBuffers); - static final IntBuffer drawBuffersColorAtt0 = nextIntBuffer(MaxDrawBuffers); - - static final IntBuffer[] drawBuffersBuffer = nextIntBufferArray(ProgramCount, MaxDrawBuffers); - - // static final String ofVersion = "OptiFine_x.x.x_HD_U_xx"; - // static final String ofVersion = Config.VERSION; - - // static int lastversion = version; - // static int updateinterval = 48; - // public static final String siteurl = "http://glslshadersof.no-ip.org"; - // public static final String updatecheckurl = "http:....../lastversion145.html"; - // static JFrame frame; - - static { - drawBuffersNone.limit(0); - drawBuffersColorAtt0.put(GL_COLOR_ATTACHMENT0_EXT).position(0).limit(1); - } - - private Shaders() {} - - /** set position to limit and advance limit by size */ - private static ByteBuffer nextByteBuffer(int size) { - ByteBuffer buffer = bigBuffer; - int pos = buffer.limit(); - buffer.position(pos).limit(pos + size); - return buffer.slice(); - } - - private static IntBuffer nextIntBuffer(int size) { - ByteBuffer buffer = bigBuffer; - int pos = buffer.limit(); - buffer.position(pos).limit(pos + size * 4); - return buffer.asIntBuffer(); - } - - private static FloatBuffer nextFloatBuffer(int size) { - ByteBuffer buffer = bigBuffer; - int pos = buffer.limit(); - buffer.position(pos).limit(pos + size * 4); - return buffer.asFloatBuffer(); - } - - private static IntBuffer[] nextIntBufferArray(int count, int size) { - IntBuffer[] aib = new IntBuffer[count]; - for (int i = 0; i < count; ++i) aib[i] = nextIntBuffer(size); - return aib; - } - - /* - * static void checkForUpdate() { if (!shadersdir.exists()) shadersdir.mkdir(); lastversion = version; int lastcheck - * = 0; File updatecheck = new File(shadersdir,"updatecheck.cfg"); try { if (updatecheck.exists()) { BufferedReader - * bufferedreader2 = new BufferedReader(new FileReader(updatecheck)); for (String s = ""; (s = - * bufferedreader2.readLine()) != null;) { try { String as[] = s.split(":"); int vv; if - * (as[0].equals("updateinterval")) { vv = Integer.parseInt(as[1]); if (vv < 0 || ( vv > 0 && vv < 24 ) || vv > 736) - * { System.out.println("[Shaders] Skipping bad option: updateinterval, range is: 0, 24-736"); } else { - * updateinterval = vv; } } else if (as[0].equals("lastcheck")) { lastcheck = Integer.parseInt(as[1]); } else if - * (as[0].equals("lastversion")) { lastversion = Integer.parseInt(as[1]); } } catch (Exception exception1) { - * System.out.println("[Shaders] "+(new StringBuilder()).append("Skipping bad option: ").append(s).toString()); } } - * bufferedreader2.close(); } } catch(Exception ex){} if (updateinterval > 0) { try { if (lastversion > version) { - * updatenotify(versiontostring(lastversion)); - * System.out.println("[Shaders] New version "+versiontostring(lastversion) - * +" of the GLSL Shaders OF mod is available, go to "+ siteurl +" to download."); } else { int currenthours = (int) - * (System.currentTimeMillis() / 3600000); if (currenthours - lastcheck > updateinterval ) { - * System.out.println("[Shaders] Checking for updates. Last checked "+Integer.toString(currenthours - - * lastcheck)+" hours ago."); lastcheck = currenthours; try { URL url = new URL( updatecheckurl ); - * HttpURLConnection.setFollowRedirects(true); HttpURLConnection uconn; uconn = (HttpURLConnection) - * url.openConnection(); uconn.setConnectTimeout(2500); uconn.setReadTimeout(1500); try { uconn.connect(); } catch - * (SocketTimeoutException excep) { System.out.println("[Shaders] Failed to connect to " + updatecheckurl); return; - * } BufferedReader bufferedreader2 = new BufferedReader(new InputStreamReader( uconn.getInputStream())); for - * (String s = ""; (s = bufferedreader2.readLine()) != null;) { try { String as[] = s.split(":"); if - * (as[1].equals("lastversion")) { lastversion = Integer.parseInt(as[2]); break; } } catch (Exception exception1){} - * } bufferedreader2.close(); } catch (Exception exception) { System.out.println("[Shaders] Failed to download " + - * updatecheckurl); return; } BufferedWriter bufferedwriter2 = new BufferedWriter(new FileWriter(updatecheck)); - * bufferedwriter2.write("updateinterval:"+updateinterval); bufferedwriter2.newLine(); - * bufferedwriter2.write("lastcheck:"+Integer.toString(lastcheck)); bufferedwriter2.newLine(); - * bufferedwriter2.write("lastversion:"+Integer.toString(lastversion)); bufferedwriter2.newLine(); - * bufferedwriter2.close(); if (lastversion > version) { updatenotify(versiontostring(lastversion)); - * System.out.println("[Shaders] New version "+versiontostring(lastversion) - * +" of the GLSL Shaders OF mod is available, go to "+ siteurl +" to download."); } } } } catch (Exception - * exception) { System.out.println("[Shaders] Failed to load GLSL Shaders update file."); - * exception.printStackTrace(); } } } public static void updatenotify(String lastversion) { try { frame = new - * JFrame("GLSL Shaders OF mod update"); } catch(Exception e) { return; } Color bgcolor=new Color(194,205,234); - * Color txcolor=new Color(50,50,50); frame.setLocation(20,20); JLabel label=new - * JLabel("New version of the GLSL Shaders OF mod is available! (v"+lastversion+") "); JPanel panel = new JPanel(); - * panel.setBackground(bgcolor); label.setForeground(txcolor); label.setFont(new Font("Dialog", 1, 14)); - * panel.add(label); if( java.awt.Desktop.isDesktopSupported() ) { JButton button; final java.awt.Desktop desktop = - * java.awt.Desktop.getDesktop(); if( desktop.isSupported( java.awt.Desktop.Action.BROWSE ) ) { button = new - * JButton("Download"); button.setActionCommand("BPUPD"); } else { button = new JButton("Copy URL to Clipboard"); - * button.setActionCommand("CPURL"); } button.setFont(new Font("Dialog", 1, 14)); button.addActionListener(new - * ButtonListener()); panel.add(button); } frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - * frame.getContentPane().add(panel); frame.pack(); frame.setFocusableWindowState(false); frame.setVisible(true); - * frame.setFocusableWindowState(true); } static class ButtonListener implements ActionListener { ButtonListener() { - * } public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (cmd == "BPUPD") { try { - * java.net.URI uri = new java.net.URI( siteurl ); java.awt.Desktop.getDesktop().browse( uri ); frame.dispose(); } - * catch ( Exception exx ) {} } else if (cmd == "CPURL") { StringSelection ss = new StringSelection(siteurl); - * Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null); } } } - */ - - public static void loadConfig() { - AngelicaTweaker.LOGGER.info("[Shaders] Loading configuration."); - try { - if (!shaderpacksdir.exists()) shaderpacksdir.mkdir(); - } catch (Exception e) { - AngelicaTweaker.LOGGER.warn("[Shaders] Failed openning shaderpacks directory."); - } - - shadersConfig = new Properties(); - shadersConfig.setProperty("shaderPack", ""); - if (configFile.exists()) { - try { - FileReader reader = new FileReader(configFile); - shadersConfig.load(reader); - reader.close(); - } catch (Exception e) {} - } - - if (!configFile.exists()) { - try { - storeConfig(); - } catch (Exception e) {} - } - configNormalMap = Boolean.parseBoolean(shadersConfig.getProperty("normalMapEnabled", "true")); - configSpecularMap = Boolean.parseBoolean(shadersConfig.getProperty("specularMapEnabled", "true")); - configTweakBlockDamage = Boolean.parseBoolean( - shadersConfig.getProperty("tweakBlockDamage", shadersConfig.getProperty("dtweak", "false"))); - configCloudShadow = Boolean.parseBoolean(shadersConfig.getProperty("cloudShadow", "true")); - configHandDepthMul = Float.parseFloat(shadersConfig.getProperty("handDepthMul", "0.125")); - configRenderResMul = Float.parseFloat(shadersConfig.getProperty("renderResMul", "1.0")); - configShadowResMul = Float.parseFloat(shadersConfig.getProperty("shadowResMul", "1.0")); - configShadowClipFrustrum = Boolean.parseBoolean(shadersConfig.getProperty("shadowClipFrustrum", "true")); - configOldLighting = Boolean.parseBoolean(shadersConfig.getProperty("oldLighting", "false")); - configTexMinFilB = Integer.parseInt(shadersConfig.getProperty("TexMinFilB", "0")) % texMinFilRange; - configTexMinFilN = Integer.parseInt(shadersConfig.getProperty("TexMinFilN", Integer.toString(configTexMinFilB))) - % texMinFilRange; - configTexMinFilS = Integer.parseInt(shadersConfig.getProperty("TexMinFilS", Integer.toString(configTexMinFilB))) - % texMinFilRange; - configTexMagFilB = Integer.parseInt(shadersConfig.getProperty("TexMagFilB", "0")) % texMagFilRange; - configTexMagFilN = Integer.parseInt(shadersConfig.getProperty("TexMagFilN", "0")) % texMagFilRange; - configTexMagFilS = Integer.parseInt(shadersConfig.getProperty("TexMagFilS", "0")) % texMagFilRange; - currentshadername = shadersConfig.getProperty("shaderPack", packNameDefault); - loadShaderPack(); - } - - public static void storeConfig() { - AngelicaTweaker.LOGGER.info("[Shaders] Save configuration."); - shadersConfig.setProperty("normalMapEnabled", Boolean.toString(configNormalMap)); - shadersConfig.setProperty("specularMapEnabled", Boolean.toString(configSpecularMap)); - shadersConfig.setProperty("tweakBlockDamage", Boolean.toString(configTweakBlockDamage)); - shadersConfig.setProperty("cloudShadow", Boolean.toString(configCloudShadow)); - shadersConfig.setProperty("handDepthMul", Float.toString(configHandDepthMul)); - shadersConfig.setProperty("renderResMul", Float.toString(configRenderResMul)); - shadersConfig.setProperty("shadowResMul", Float.toString(configShadowResMul)); - shadersConfig.setProperty("shadowClipFrustrum", Boolean.toString(configShadowClipFrustrum)); - shadersConfig.setProperty("oldLighting", Boolean.toString(configOldLighting)); - shadersConfig.setProperty("TexMinFilB", Integer.toString(configTexMinFilB)); - shadersConfig.setProperty("TexMinFilN", Integer.toString(configTexMinFilN)); - shadersConfig.setProperty("TexMinFilS", Integer.toString(configTexMinFilS)); - shadersConfig.setProperty("TexMagFilB", Integer.toString(configTexMagFilB)); - shadersConfig.setProperty("TexMagFilN", Integer.toString(configTexMagFilN)); - shadersConfig.setProperty("TexMagFilS", Integer.toString(configTexMagFilS)); - try { - FileWriter writer = new FileWriter(configFile); - shadersConfig.store(writer, null); - writer.close(); - } catch (Exception ex) {} - } - - public static void setShaderPack(String par1name) { - currentshadername = par1name; - shadersConfig.setProperty("shaderPack", par1name); - // loadShaderPack(); - } - - public static void loadShaderPack() { - if (shaderPack != null) { - shaderPack.close(); - shaderPack = null; - } - String packName = shadersConfig.getProperty("shaderPack", packNameDefault); - if (!packName.isEmpty() && !packName.equals(packNameNone)) { - if (packName.equals(packNameDefault)) { - shaderPack = new ShaderPackDefault(); - } else { - try { - File packFile = new File(shaderpacksdir, packName); - if (packFile.isDirectory()) { - shaderPack = new ShaderPackFolder(packName, packFile); - } else if (packFile.isFile() && packName.toLowerCase().endsWith(".zip")) { - shaderPack = new ShaderPackZip(packName, packFile); - } - } catch (Exception e) {} - } - } - if (shaderPack != null) { - AngelicaTweaker.LOGGER.info("[Shaders] Loaded shaderpack."); - } else { - AngelicaTweaker.LOGGER.error("[Shaders] Did not load shaderpack."); - shaderPack = new ShaderPackNone(); - } - } - - // public static void loadShader0() - // { - // Set shaderfiles = new HashSet(); - // for (int si = 1; si < 10; si++) - // { - // shaderfiles.add(programNames[si]+".fsh"); - // shaderfiles.add(programNames[si]+".vsh"); - // } - // - // try - // { - // - // ZipInputStream inp = new ZipInputStream(new FileInputStream(currentshader)); - // System.out.println("[Shaders] Shader selected: " + currentshadername); - // boolean found = false; - // - // ZipEntry entry = inp.getNextEntry(); - // while(entry != null ) - // { - // if (entry.isDirectory()) - // { - // if (entry.getName().toLowerCase().contains("shaders/contents/files/shaders/")) - // { - // found = true; - // break; - // } - // } - // entry = inp.getNextEntry(); - // } - // - // if (!found) - // { - // inp.close(); - // inp = new ZipInputStream(new FileInputStream(currentshader)); - // entry = inp.getNextEntry(); - // while(entry != null ) - // { - // if (entry.isDirectory()) - // { - // if (entry.getName().toLowerCase().contains("shaders/")) - // { - // found = true; - // break; - // } - // } - // entry = inp.getNextEntry(); - // } - // } - // - // if (!found) - // { - // inp.close(); - // inp = new ZipInputStream(new FileInputStream(currentshader)); - // entry = inp.getNextEntry(); - // } - // - // byte[] buf = new byte[1024]; - // int n; - // String zipfile; - // String zippath = ""; - // String zippathfile; - // - // FileOutputStream fileoutputstream, fileoutputstream2; - // boolean seusrc5tweak = false; - // - // while(entry != null && shaderfiles.size() > 0) - // { - // zipfile = new File(entry.getName()).getName(); - // if (shaderfiles.contains(zipfile) && !entry.isDirectory()) - // { - // zippathfile = new File(entry.getName()).getPath(); - // if (shaderfiles.size() == 18) - // { - // zippath = zippathfile.substring(0,zippathfile.length()-zipfile.length()); - // } - // if (zippathfile.equals(zippath+zipfile)) - // { - // shaderfiles.remove(zipfile); - // if (zipfile.equals("gbuffers_water.vsh") && entry.getCrc() == 2530345120L ) seusrc5tweak = true; - // - // fileoutputstream = new FileOutputStream(shadersdir + File.separator + "temp" + File.separator + zipfile); - // while ((n = inp.read(buf, 0, 1024)) > -1) fileoutputstream.write(buf, 0, n); - // fileoutputstream.close(); - // System.out.println("[Shaders] Shader loaded: " + zippathfile); - // } - // } - // entry = inp.getNextEntry(); - // } - // inp.close(); - // - // if (seusrc5tweak) - // { - // new File(shadersdir + File.separator + "temp" + File.separator + "gbuffers_water.fsh").delete(); - // new File(shadersdir + File.separator + "temp" + File.separator + "gbuffers_water.vsh").delete(); - // FileInputStream file = new FileInputStream(shadersdir + File.separator + "temp" + File.separator + - // "gbuffers_textured_lit.fsh"); - // FileOutputStream file2 = new FileOutputStream(shadersdir + File.separator + "temp" + File.separator + - // "gbuffers_water.fsh",false); - // while ((n = file.read(buf, 0, 1024)) > -1) file2.write(buf, 0, n); - // file.close(); - // file2.close(); - // file = new FileInputStream(shadersdir + File.separator + "temp" + File.separator + - // "gbuffers_textured_lit.vsh"); - // file2 = new FileOutputStream(shadersdir + File.separator + "temp" + File.separator + - // "gbuffers_water.vsh",false); - // while ((n = file.read(buf, 0, 1024)) > -1) file2.write(buf, 0, n); - // file.close(); - // file2.close(); - // System.out.println("[Shaders] Detected SEUS v10 RC5, copied gbuffers_textured_lit. to gbuffers_water."); - // } - // } - // catch (Exception exc) - // {} - // - // isInitialized = false; - // if (shaderfiles.size() == 18 ) System.out.println("[Shaders] No Shader found in: " + currentshadername); - // else System.out.println("[Shaders] Shader loaded: " + currentshadername); - // } - - static List listofShaders() { - List list = new ArrayList<>(); - list.add(packNameNone); - list.add(packNameDefault); - try { - if (!shaderpacksdir.exists()) shaderpacksdir.mkdir(); - File[] listOfFiles = shaderpacksdir.listFiles(); - for (int i = 0; i < listOfFiles.length; i++) { - File file = listOfFiles[i]; - String name = file.getName(); - if (file.isDirectory() || file.isFile() && name.toLowerCase().endsWith(".zip")) { - list.add(name); - } - } - } catch (Exception e) {} - return list; - } - - static String versiontostring(int vv) { - String vs = Integer.toString(vv); - return Integer.toString(Integer.parseInt(vs.substring(1, 3))) + "." - + Integer.toString(Integer.parseInt(vs.substring(3, 5))) - + "." - + Integer.toString(Integer.parseInt(vs.substring(5))); - } - - static void checkOptifine() { - /* - * try { System.out.println("[Shaders] Required OptiFine version : " + ofVersion); String configVersion = - * (String)(Config.class.getDeclaredField("VERSION").get(null)); - * System.out.println("[Shaders] Detected OptiFine version : "+ configVersion); if - * (configVersion.equals(ofVersion) ) { System.out.println("[Shaders] ShadersMod loaded. version: " + - * versiontostring(version)); } else { System.err.println("[Shaders] Wrong OptiFine version!"); System.exit(-1); - * } } catch(Exception e) { System.err.println("[Shaders] OptiFine missing or wrong version! Install OptiFine " - * +ofVersion+" first and then the ShadersMod!"); System.exit(-1); } //Tessellator.shaders = true; - */ - } - - public static int checkFramebufferStatus(String location) { - int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - if (status != GL_FRAMEBUFFER_COMPLETE_EXT) - AngelicaTweaker.LOGGER.error(String.format("FramebufferStatus 0x%04X at %s", status, location)); - return status; - } - - public static int checkGLError(String location) { - int errorCode = glGetError(); - if (errorCode != GL_NO_ERROR) { - boolean skipPrint = false; - // if (location.equals("updatechunks")) { - // if (updateChunksErrorRecorded) { - // skipPrint = true; - // } else { - // updateChunksErrorRecorded = true; - // } - // } - if (!skipPrint) { - if (errorCode == GL_INVALID_FRAMEBUFFER_OPERATION_EXT) { - int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - AngelicaTweaker.LOGGER.error( - String.format( - "GL error 0x%04X: %s (Fb status 0x%04X) at %s", - errorCode, - gluErrorString(errorCode), - status, - location)); - } else { - AngelicaTweaker.LOGGER.error( - String.format("GL error 0x%04X: %s at %s", errorCode, gluErrorString(errorCode), location)); - } - } - } - return errorCode; - } - - public static int checkGLError(String location, String info) { - int errorCode = glGetError(); - if (errorCode != GL_NO_ERROR) { - AngelicaTweaker.LOGGER.error( - String.format( - "GL error 0x%04x: %s at %s %s", - errorCode, - gluErrorString(errorCode), - location, - info)); - } - return errorCode; - } - - public static int checkGLError(String location, String info1, String info2) { - int errorCode = glGetError(); - if (errorCode != GL_NO_ERROR) { - AngelicaTweaker.LOGGER.error( - String.format( - "GL error 0x%04x: %s at %s %s %s", - errorCode, - gluErrorString(errorCode), - location, - info1, - info2)); - } - return errorCode; - } - - private static String printChatAndLogError(String str) { - mc.ingameGUI.getChatGUI().printChatMessage(new ChatComponentText(str)); - AngelicaTweaker.LOGGER.error(str); - return str; - } - - public static void printIntBuffer(String title, IntBuffer buf) { - StringBuilder sb = new StringBuilder(128); - sb.append(title).append(" [pos ").append(buf.position()).append(" lim ").append(buf.limit()).append(" cap ") - .append(buf.capacity()).append(" :"); - for (int lim = buf.limit(), i = 0; i < lim; ++i) sb.append(" ").append(buf.get(i)); - sb.append("]"); - AngelicaTweaker.LOGGER.debug(sb.toString()); - } - - public static void startup(Minecraft mc) { - Shaders.mc = mc; - AngelicaTweaker.LOGGER.info("Angelica version " + versionString); - Shaders.loadConfig(); - } - - private static String toStringYN(boolean b) { - return b ? "Y" : "N"; - } - - public static void updateBlockLightLevel() { - if (configOldLighting) { - blockLightLevel05 = 0.5f; - blockLightLevel06 = 0.6f; - blockLightLevel08 = 0.8f; - } else { - blockLightLevel05 = 1.0f; - blockLightLevel06 = 1.0f; - blockLightLevel08 = 1.0f; - } - } - - public static void init() { - if (!isInitialized) { - mc = Minecraft.getMinecraft(); - checkGLError("Shaders.init pre"); - capabilities = GLContext.getCapabilities(); - AngelicaTweaker.LOGGER.debug( - "[Shaders] OpenGL 2.0 = " + toStringYN(capabilities.OpenGL20) - + " 2.1 = " - + toStringYN(capabilities.OpenGL21) - + " 3.0 = " - + toStringYN(capabilities.OpenGL30) - + " 3.2 = " - + toStringYN(capabilities.OpenGL32)); - if (!capabilities.OpenGL21) { - printChatAndLogError("[Shaders] No OpenGL 2.1."); - } - if (!capabilities.GL_EXT_framebuffer_object) { - printChatAndLogError("[Shaders] No EXT_framebuffer_object."); - } - if (!capabilities.OpenGL20 || !capabilities.GL_EXT_framebuffer_object) { - printChatAndLogError("[Shaders] Your GPU is not compatible with the Shaders mod."); - } - - hasGlGenMipmap = capabilities.OpenGL30; - dfbDrawBuffers.position(0).limit(MaxDrawBuffers); - dfbColorTextures.position(0).limit(MaxColorBuffers); - dfbDepthTextures.position(0).limit(MaxDepthBuffers); - sfbDrawBuffers.position(0).limit(MaxDrawBuffers); - sfbDepthTextures.position(0).limit(MaxShadowDepthBuffers); - sfbColorTextures.position(0).limit(MaxShadowColorBuffers); - - int maxDrawBuffers = glGetInteger(GL_MAX_DRAW_BUFFERS); - int maxColorAttach = glGetInteger(GL_MAX_COLOR_ATTACHMENTS_EXT); - AngelicaTweaker.LOGGER.debug("[Shaders] GL_MAX_DRAW_BUFFERS = " + maxDrawBuffers); - AngelicaTweaker.LOGGER.debug("[Shaders] GL_MAX_COLOR_ATTACHMENTS_EXT = " + maxColorAttach); - AngelicaTweaker.LOGGER - .debug("[Shaders] GL_MAX_TEXTURE_IMAGE_UNITS = " + glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)); - - usedColorBuffers = 4; - usedDepthBuffers = 1; - usedShadowColorBuffers = 0; - usedShadowDepthBuffers = 0; - usedColorAttachs = 1; - usedDrawBuffers = 1; - Arrays.fill(gbuffersFormat, GL_RGBA); - Arrays.fill(shadowHardwareFilteringEnabled, false); - Arrays.fill(shadowMipmapEnabled, false); - Arrays.fill(shadowFilterNearest, false); - Arrays.fill(shadowColorMipmapEnabled, false); - Arrays.fill(shadowColorFilterNearest, false); - centerDepthSmoothEnabled = false; - noiseTextureEnabled = false; - sunPathRotation = 0f; - shadowIntervalSize = 2.0f; - aoLevel = 0.8f; - blockAoLight = 1.0f - aoLevel; - useEntityAttrib = false; - useMidTexCoordAttrib = false; - useMultiTexCoord3Attrib = false; - waterShadowEnabled = false; - updateChunksErrorRecorded = false; - updateBlockLightLevel(); - - for (int i = 0; i < ProgramCount; ++i) { - if (programNames[i] == "") { - programsID[i] = programsRef[i] = 0; - programsDrawBufSettings[i] = null; - programsColorAtmSettings[i] = null; - programsCompositeMipmapSetting[i] = 0; - } else { - newDrawBufSetting = null; - newColorAtmSetting = null; - newCompositeMipmapSetting = 0; - int pr = setupProgram( - i, - "/shaders/" + programNames[i] + ".vsh", - "/shaders/" + programNames[i] + ".fsh"); - programsID[i] = programsRef[i] = pr; - programsDrawBufSettings[i] = (pr != 0) ? newDrawBufSetting : null; - programsColorAtmSettings[i] = (pr != 0) ? newColorAtmSetting : null; - programsCompositeMipmapSetting[i] = (pr != 0) ? newCompositeMipmapSetting : 0; - } - } - - Map drawBuffersMap = new HashMap<>(); - for (int p = 0; p < ProgramCount; ++p) { - if (p == ProgramFinal) { - programsDrawBuffers[p] = null; - } else if (programsID[p] == 0) { - if (p == ProgramShadow) { - programsDrawBuffers[p] = drawBuffersNone; - } else { - programsDrawBuffers[p] = drawBuffersColorAtt0; - } - } else { - String str = programsDrawBufSettings[p]; - if (str != null) { - IntBuffer intbuf = drawBuffersBuffer[p]; - int numDB = str.length(); - if (numDB > usedDrawBuffers) { - usedDrawBuffers = numDB; - } - if (numDB > maxDrawBuffers) { - numDB = maxDrawBuffers; - } - programsDrawBuffers[p] = intbuf; - intbuf.limit(numDB); - for (int i = 0; i < numDB; ++i) { - int d = GL_NONE; - if (str.length() > i) { - int ca = str.charAt(i) - '0'; - if (p != ProgramShadow) { - if (ca >= 0 && ca <= 7) { - d = ca + GL_COLOR_ATTACHMENT0_EXT; - if (ca > usedColorAttachs) { - usedColorAttachs = ca; - } - if (ca > usedColorBuffers) { - usedColorBuffers = ca; - } - } - } else { - if (ca >= 0 && ca <= 1) { - d = ca + GL_COLOR_ATTACHMENT0_EXT; - if (ca > usedShadowColorBuffers) { - usedShadowColorBuffers = ca; - } - } - } - } - intbuf.put(i, d); - } - } else { - if (p != ProgramShadow) { - programsDrawBuffers[p] = dfbDrawBuffers; - usedDrawBuffers = usedColorBuffers; - } else { - programsDrawBuffers[p] = sfbDrawBuffers; - } - } - } - } - - // if (programIDCopyDepth==0) - // programIDCopyDepth = setupProgramCopyDepth(); - - // TO DO: add color attachment option - usedColorAttachs = usedColorBuffers; - - shadowPassInterval = (usedShadowDepthBuffers > 0) ? 1 : 0; - shouldSkipDefaultShadow = (usedShadowDepthBuffers > 0); - - dfbDrawBuffers.position(0).limit(usedDrawBuffers); - dfbColorTextures.position(0).limit(usedColorBuffers); - // dfbRenderBuffers.limit(colorAttachments); - - for (int i = 0; i < usedDrawBuffers; ++i) { - dfbDrawBuffers.put(i, GL_COLOR_ATTACHMENT0_EXT + i); - } - - if (usedDrawBuffers > maxDrawBuffers) { - printChatAndLogError( - "[Shaders] Not enough draw buffers! Requires " + usedDrawBuffers - + ". Has " - + maxDrawBuffers - + "."); - } - // if (usedColorBuffers > maxColorAttach) { - // printChatAndLogError("[Shaders] Not enough color attachment! Requires "+ usedColorBuffers + ". Has " + - // maxColorAttach + "."); - // } - - sfbDrawBuffers.position(0).limit(usedShadowColorBuffers); - for (int i = 0; i < usedShadowColorBuffers; ++i) { - sfbDrawBuffers.put(i, GL_COLOR_ATTACHMENT0_EXT + i); - } - - // Use programBackups for missing programs - for (int i = 0; i < ProgramCount; ++i) { - int n = i; - while (programsID[n] == 0 && programBackups[n] != n) n = programBackups[n]; - if (n != i && i != ProgramShadow) { - programsID[i] = programsID[n]; - programsDrawBufSettings[i] = programsDrawBufSettings[n]; - programsDrawBuffers[i] = programsDrawBuffers[n]; - } - } - - // printIntBuffer("dfb drawbuffers",dfbDrawBuffers); - // printIntBuffer("sfb drawbuffers",sfbDrawBuffers); - // printIntBuffer("pgshadow drawbf",programsDrawBuffers[ProgramShadow]); - - // setup scene frame buffer and textures - resize(); - - // setup shadow frame buffer and texture - resizeShadow(); - - if (noiseTextureEnabled) setupNoiseTexture(); - - if (defaultTexture == null) defaultTexture = ShadersTex.createDefaultTexture(); - // TextureNM texBlocks = (TextureNM)textureManager.textureMapBlocks.getTexture(); - // TextureNM texItems = (TextureNM)textureManager.textureMapBlocks.getTexture(); - // terrainTextureIdArray = new int[] { texBlocks.getGlTextureId(), texBlocks.normalMap.getGlTextureId(), - // texBlocks.specularMap.getGlTextureId() }; - // textureIdMap.put(terrainTextureIdArray[0], terrainTextureIdArray); - // int[] itemsTextureIdArray; - // itemsTextureIdArray = new int[] { texItems.getGlTextureId(), texItems.normalMap.getGlTextureId(), - // texItems.specularMap.getGlTextureId() }; - // textureIdMap.put(itemsTextureIdArray[0], itemsTextureIdArray); - - isInitialized = true; - - resetDisplayList(); - if (notFirstInit) { - mc.ingameGUI.getChatGUI().printChatMessage(new ChatComponentText("Shaders initialized.")); - } - checkGLError("Shaders.init"); - } - } - - public static void resetDisplayList() { - ++numberResetDisplayList; - AngelicaTweaker.LOGGER.debug("Reset model renderers"); - if (Shaders.useMidTexCoordAttrib || Shaders.useMultiTexCoord3Attrib) { - Iterator it = RenderManager.instance.entityRenderMap.values().iterator(); - while (it.hasNext()) { - Render ren = it.next(); - if (ren instanceof RendererLivingEntity) { - RendererLivingEntity rle = (RendererLivingEntity) ren; - // System.out.format("Reset %s\n", rle.toString()); - resetDisplayListModel(rle.mainModel); - resetDisplayListModel(rle.renderPassModel); - } - } - } - AngelicaTweaker.LOGGER.debug("Reset world renderers"); - mc.renderGlobal.loadRenderers(); - } - - public static void resetDisplayListModel(ModelBase mbase) { - if (mbase != null) { - Iterator it = mbase.boxList.iterator(); - while (it.hasNext()) { - ModelRenderer obj = it.next(); - if (obj != null) { - resetDisplayListModelRenderer(obj); - } - } - } - } - - public static void resetDisplayListModelRenderer(ModelRenderer mrr) { - ((IModelRenderer) mrr).angelica$resetDisplayList(); - // if (mrr.compiled) - // { - // GLAllocation.deleteDisplayLists(mrr.displayList); - // mrr.displayList = 0; - // mrr.compiled = false; - // if (mrr instanceof ModelRotationRenderer) { - // ModelRotationRenderer mrt = (ModelRotationRenderer)mrr; - // mrt.compiled = false; - // } - // } - if (mrr.childModels != null) { - for (int i = 0, n = mrr.childModels.size(); i < n; ++i) { - resetDisplayListModelRenderer((ModelRenderer) mrr.childModels.get(i)); - } - } - } - - // ---------------------------------------- - - // private static final String shaderCodeCopyDepthV = - // "#version 120\n"+ - // "varying vec4 texcoord;\n"+ - // "void main()\n"+ - // "{\n"+ - // " texcoord = gl_MultiTexCoord0;\n"+ - // " gl_Position = ftransform();\n"+ - // "}\n" - // ; - // private static final String shaderCodeCopyDepthF = - // "#version 120\n"+ - // "varying vec4 texcoord;\n"+ - // "uniform sampler2D depthtex0;\n"+ - // "void main()\n"+ - // "{\n"+ - // " gl_FragDepth = texture2D(depthtex0,texcoord.st).r;\n"+ - // " gl_FragColor = texture2D(depthtex0,texcoord.st);\n"+ - // "}\n" - // ; - // - // private static int setupProgramCopyDepth() { - // int program = glCreateProgramObjectARB(); - // if (program !=0) { - // int vShader = createVertShaderFromCode(shaderCodeCopyDepthV); - // int fShader = createFragShaderFromCode(shaderCodeCopyDepthF); - // if (vShader != 0 || fShader != 0) { - // if (vShader != 0) { - // glAttachObjectARB(program, vShader); - // } - // if (fShader != 0) { - // glAttachObjectARB(program, fShader); - // } - // glLinkProgramARB(program); - // if (vShader != 0) { - // glDetachObjectARB(program, vShader); - // glDeleteObjectARB(vShader); - // } - // if (fShader != 0) { - // glDetachObjectARB(program, fShader); - // glDeleteObjectARB(fShader); - // } - // int valid = glGetProgrami(program, GL_VALIDATE_STATUS); // glGetProgrami in LWJGL 3.0+ - // if (valid == GL_TRUE) { - // System.out.println("Program copydepth created"); - // } else { - // printChatAndLogError("[Shaders] Error : Invalid program copydepth"); - // glDeleteObjectARB(program); - // program = 0; - // } - // } else { - // glDeleteObjectARB(program); - // program = 0; - // } - // } - // return program; - // } - // - // - // private static int createVertShaderFromCode(String code) { - // int vertShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB); - // if (vertShader == 0) { - // return 0; - // } - // glShaderSourceARB(vertShader, code); - // glCompileShaderARB(vertShader); - // printLogInfo(vertShader,"copydepth vert"); - // return vertShader; - // } - // - // - // private static int createFragShaderFromCode(String code) { - // int fragShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB); - // if (fragShader == 0) { - // return 0; - // } - // glShaderSourceARB(fragShader, code); - // glCompileShaderARB(fragShader); - // printLogInfo(fragShader,"copydepth frag"); - // return fragShader; - // } - - private static int setupProgram(int program, String vShaderPath, String fShaderPath) { - checkGLError("pre setupProgram"); - int programid = glCreateProgramObjectARB(); - checkGLError("create"); - if (programid != 0) { - progUseEntityAttrib = false; - progUseMidTexCoordAttrib = false; - int vShader = createVertShader(vShaderPath); - int fShader = createFragShader(fShaderPath); - checkGLError("create"); - if (vShader != 0 || fShader != 0) { - if (vShader != 0) { - glAttachObjectARB(programid, vShader); - checkGLError("attach"); - } - if (fShader != 0) { - glAttachObjectARB(programid, fShader); - checkGLError("attach"); - } - if (progUseEntityAttrib) { - glBindAttribLocationARB(programid, entityAttrib, "mc_Entity"); - checkGLError("mc_Entity"); - } - if (progUseMidTexCoordAttrib) { - glBindAttribLocationARB(programid, midTexCoordAttrib, "mc_midTexCoord"); - checkGLError("mc_midTexCoord"); - } - glLinkProgramARB(programid); - if (vShader != 0) { - glDetachObjectARB(programid, vShader); - glDeleteObjectARB(vShader); - } - if (fShader != 0) { - glDetachObjectARB(programid, fShader); - glDeleteObjectARB(fShader); - } - programsID[program] = programid; - useProgram(program); - glValidateProgramARB(programid); - useProgram(ProgramNone); - printLogInfo(programid, vShaderPath + "," + fShaderPath); - int valid = glGetProgrami(programid, GL_VALIDATE_STATUS); - if (valid == GL_TRUE) { - AngelicaTweaker.LOGGER.debug("Program " + programNames[program] + " loaded"); - } else { - printChatAndLogError("[Shaders] Error : Invalid program " + programNames[program]); - glDeleteObjectARB(programid); - programid = 0; - } - } else { - glDeleteObjectARB(programid); - programid = 0; - } - } - return programid; - } - - private static InputStream locateShaderStream(String path) { - try { - return shaderPack.getResourceAsStream(path); - } catch (Exception e) { - try { - return FileUtils.openInputStream(new File(path)); - } catch (Exception e2) { - throw new RuntimeException("Could not locate shader input " + path, e); - } - } - } - - private static String getPreprocessedShaderSources(String filename) { - filename = filename.replace('\\', '/'); - int lastSlash = filename.lastIndexOf('/'); - final String basename = (lastSlash == -1) ? "/" : filename.substring(0, lastSlash + 1); - try (InputStream is = locateShaderStream(filename)) { - if (is == null) { - throw new FileNotFoundException(filename); - } - String source = IOUtils.toString(is, StandardCharsets.UTF_8).replace("\r\n", "\n"); - StringBuffer output = new StringBuffer(2 * source.length() + 64); - - final Matcher includes = INCLUDE_PATTERN.matcher(source); - while (includes.find()) { - final String relPath = includes.group(1).replace('\\', '/'); - final String path = relPath.startsWith("/") ? ("/shaders" + relPath) : (basename + relPath); - final String src = getPreprocessedShaderSources(path); - includes.appendReplacement(output, src); - } - includes.appendTail(output); - return output.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static int createVertShader(String filename) { - int vertShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB); - if (vertShader == 0) { - return 0; - } - String shaderSrc; - try { - shaderSrc = getPreprocessedShaderSources(filename); - - for (String line : StringUtils.split(shaderSrc, '\n')) { - processVertShaderLine(line); - } - } catch (Exception e) { - e.printStackTrace(); - glDeleteObjectARB(vertShader); - return 0; - } - - glShaderSourceARB(vertShader, shaderSrc); - glCompileShaderARB(vertShader); - printLogInfo(vertShader, filename); - return vertShader; - } - - private static void processVertShaderLine(String line) { - if (line.matches("attribute [_a-zA-Z0-9]+ mc_Entity.*")) { - useEntityAttrib = true; - progUseEntityAttrib = true; - } else if (line.matches("attribute [_a-zA-Z0-9]+ mc_midTexCoord.*")) { - useMidTexCoordAttrib = true; - progUseMidTexCoordAttrib = true; - } else if (line.matches(".*gl_MultiTexCoord3.*")) { - useMultiTexCoord3Attrib = true; - } - } - - private static final Pattern gbufferFormatPattern = Pattern - .compile("[ \t]*const[ \t]*int[ \t]*(\\w+)Format[ \t]*=[ \t]*([RGBA81632F]*)[ \t]*;.*"); - private static final Pattern gbufferMipmapEnabledPattern = Pattern - .compile("[ \t]*const[ \t]*bool[ \t]*(\\w+)MipmapEnabled[ \t]*=[ \t]*true[ \t]*;.*"); - - private static final Pattern INCLUDE_PATTERN = Pattern - .compile("^\\s*#include\\s+\"([A-Za-z0-9_\\/\\.]+)\".*$", Pattern.MULTILINE | Pattern.UNIX_LINES); - - private static int createFragShader(String filename) { - int fragShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB); - if (fragShader == 0) { - return 0; - } - String shaderSrc; - try { - shaderSrc = getPreprocessedShaderSources(filename); - - for (String line : StringUtils.split(shaderSrc, '\n')) { - processFragShaderLine(line, filename); - } - } catch (Exception e) { - e.printStackTrace(); - glDeleteObjectARB(fragShader); - return 0; - } - - glShaderSourceARB(fragShader, shaderSrc); - glCompileShaderARB(fragShader); - printLogInfo(fragShader, filename); - return fragShader; - } - - // TODO: refactor this mess - private static void processFragShaderLine(String line, String filename) { - if (line.matches("#version .*")) { - - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadow;.*")) { - if (usedShadowDepthBuffers < 1) usedShadowDepthBuffers = 1; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ watershadow;.*")) { - waterShadowEnabled = true; - if (usedShadowDepthBuffers < 2) usedShadowDepthBuffers = 2; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadowtex0;.*")) { - if (usedShadowDepthBuffers < 1) usedShadowDepthBuffers = 1; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadowtex1;.*")) { - if (usedShadowDepthBuffers < 2) usedShadowDepthBuffers = 2; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadowcolor;.*")) { - if (usedShadowColorBuffers < 1) usedShadowColorBuffers = 1; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadowcolor0;.*")) { - if (usedShadowColorBuffers < 1) usedShadowColorBuffers = 1; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ shadowcolor1;.*")) { - if (usedShadowColorBuffers < 2) usedShadowColorBuffers = 2; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ depthtex0;.*")) { - if (usedDepthBuffers < 1) usedDepthBuffers = 1; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ depthtex1;.*")) { - if (usedDepthBuffers < 2) usedDepthBuffers = 2; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ depthtex2;.*")) { - if (usedDepthBuffers < 3) usedDepthBuffers = 3; - } else if (line.matches("uniform [ _a-zA-Z0-9]+ gdepth;.*")) { - if (gbuffersFormat[1] == GL_RGBA) gbuffersFormat[1] = GL_RGBA32F; - } else if (usedColorBuffers < 5 && line.matches("uniform [ _a-zA-Z0-9]+ gaux1;.*")) { - usedColorBuffers = 5; - } else if (usedColorBuffers < 6 && line.matches("uniform [ _a-zA-Z0-9]+ gaux2;.*")) { - usedColorBuffers = 6; - } else if (usedColorBuffers < 7 && line.matches("uniform [ _a-zA-Z0-9]+ gaux3;.*")) { - usedColorBuffers = 7; - } else if (usedColorBuffers < 8 && line.matches("uniform [ _a-zA-Z0-9]+ gaux4;.*")) { - usedColorBuffers = 8; - } else if (usedColorBuffers < 5 && line.matches("uniform [ _a-zA-Z0-9]+ colortex4;.*")) { - usedColorBuffers = 5; - } else if (usedColorBuffers < 6 && line.matches("uniform [ _a-zA-Z0-9]+ colortex5;.*")) { - usedColorBuffers = 6; - } else if (usedColorBuffers < 7 && line.matches("uniform [ _a-zA-Z0-9]+ colortex6;.*")) { - usedColorBuffers = 7; - } else if (usedColorBuffers < 8 && line.matches("uniform [ _a-zA-Z0-9]+ colortex7;.*")) { - usedColorBuffers = 8; - - } else if (usedColorBuffers < 8 && line.matches("uniform [ _a-zA-Z0-9]+ centerDepthSmooth;.*")) { - centerDepthSmoothEnabled = true; - - // Shadow resolution - } else if (line.matches("/\\* SHADOWRES:[0-9]+ \\*/.*")) { - String[] parts = line.split("(:| )", 4); - AngelicaTweaker.LOGGER.debug("Shadow map resolution: " + parts[2]); - spShadowMapWidth = spShadowMapHeight = Integer.parseInt(parts[2]); - shadowMapWidth = shadowMapHeight = Math.round(spShadowMapWidth * configShadowResMul); - - } else if (line.matches("[ \t]*const[ \t]*int[ \t]*shadowMapResolution[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER.debug("Shadow map resolution: " + parts[1]); - spShadowMapWidth = spShadowMapHeight = Integer.parseInt(parts[1]); - shadowMapWidth = shadowMapHeight = Math.round(spShadowMapWidth * configShadowResMul); - - } else if (line.matches("/\\* SHADOWFOV:[0-9\\.]+ \\*/.*")) { - String[] parts = line.split("(:| )", 4); - AngelicaTweaker.LOGGER.debug("Shadow map field of view: " + parts[2]); - shadowMapFOV = Float.parseFloat(parts[2]); - shadowMapIsOrtho = false; - - // Shadow distance - } else if (line.matches("/\\* SHADOWHPL:[0-9\\.]+ \\*/.*")) { - String[] parts = line.split("(:| )", 4); - AngelicaTweaker.LOGGER.debug("Shadow map half-plane: " + parts[2]); - shadowMapHalfPlane = Float.parseFloat(parts[2]); - shadowMapIsOrtho = true; - - } else if (line.matches("[ \t]*const[ \t]*float[ \t]*shadowDistance[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER.debug("Shadow map distance: " + parts[1]); - shadowMapHalfPlane = Float.parseFloat(parts[1]); - shadowMapIsOrtho = true; - - } else if (line.matches("[ \t]*const[ \t]*float[ \t]*shadowIntervalSize[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER.debug("Shadow map interval size: " + parts[1]); - shadowIntervalSize = Float.parseFloat(parts[1]); - - } else if (line.matches("[ \t]*const[ \t]*bool[ \t]*generateShadowMipmap[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("Generate shadow mipmap"); - Arrays.fill(shadowMipmapEnabled, true); - - } else if (line.matches("[ \t]*const[ \t]*bool[ \t]*generateShadowColorMipmap[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("Generate shadow color mipmap"); - Arrays.fill(shadowColorMipmapEnabled, true); - - } else if (line.matches("[ \t]*const[ \t]*bool[ \t]*shadowHardwareFiltering[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("Hardware shadow filtering enabled."); - Arrays.fill(shadowHardwareFilteringEnabled, true); - - } else if (line.matches("[ \t]*const[ \t]*bool[ \t]*shadowHardwareFiltering0[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowHardwareFiltering0"); - shadowHardwareFilteringEnabled[0] = true; - - } else if (line.matches("[ \t]*const[ \t]*bool[ \t]*shadowHardwareFiltering1[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowHardwareFiltering1"); - shadowHardwareFilteringEnabled[1] = true; - - } else if (line - .matches("[ \t]*const[ \t]*bool[ \t]*(shadowtex0Mipmap|shadowtexMipmap)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowtex0Mipmap"); - shadowMipmapEnabled[0] = true; - - } else - if (line.matches("[ \t]*const[ \t]*bool[ \t]*(shadowtex1Mipmap)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowtex1Mipmap"); - shadowMipmapEnabled[1] = true; - - } else if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowcolor0Mipmap|shadowColor0Mipmap)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowcolor0Mipmap"); - shadowColorMipmapEnabled[0] = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowcolor1Mipmap|shadowColor1Mipmap)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowcolor1Mipmap"); - shadowColorMipmapEnabled[1] = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowtex0Nearest|shadowtexNearest|shadow0MinMagNearest)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowtex0Nearest"); - shadowFilterNearest[0] = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowtex1Nearest|shadow1MinMagNearest)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowtex1Nearest"); - shadowFilterNearest[1] = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowcolor0Nearest|shadowColor0Nearest|shadowColor0MinMagNearest)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowcolor0Nearest"); - shadowColorFilterNearest[0] = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*(shadowcolor1Nearest|shadowColor1Nearest|shadowColor1MinMagNearest)[ \t]*=[ \t]*true[ \t]*;.*")) { - AngelicaTweaker.LOGGER.debug("shadowcolor1Nearest"); - shadowColorFilterNearest[1] = true; - - // Wetness half life - } else - if (line.matches("/\\* WETNESSHL:[0-9\\.]+ \\*/.*")) { - String[] parts = line.split("(:| )", 4); - AngelicaTweaker.LOGGER.debug("Wetness halflife: " + parts[2]); - wetnessHalfLife = Float.parseFloat(parts[2]); - - } else if (line.matches( - "[ \t]*const[ \t]*float[ \t]*wetnessHalflife[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER.debug("Wetness halflife: " + parts[1]); - wetnessHalfLife = Float.parseFloat(parts[1]); - - // Dryness halflife - } else - if (line.matches("/\\* DRYNESSHL:[0-9\\.]+ \\*/.*")) { - String[] parts = line.split("(:| )", 4); - AngelicaTweaker.LOGGER.debug("Dryness halflife: " + parts[2]); - drynessHalfLife = Float.parseFloat(parts[2]); - - } else if (line.matches( - "[ \t]*const[ \t]*float[ \t]*drynessHalflife[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER.debug("Dryness halflife: " + parts[1]); - drynessHalfLife = Float.parseFloat(parts[1]); - - // Eye brightness halflife - } else - if (line.matches( - "[ \t]*const[ \t]*float[ \t]*eyeBrightnessHalflife[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER - .debug("Eye brightness halflife: " + parts[1]); - eyeBrightnessHalflife = Float.parseFloat(parts[1]); - - // Center depth halflife - } else - if (line.matches( - "[ \t]*const[ \t]*float[ \t]*centerDepthHalflife[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER - .debug("Center depth halflife: " + parts[1]); - centerDepthSmoothHalflife = Float.parseFloat(parts[1]); - - // Sun path rotation - } else - if (line.matches( - "[ \t]*const[ \t]*float[ \t]*sunPathRotation[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER - .debug("Sun path rotation: " + parts[1]); - sunPathRotation = Float.parseFloat(parts[1]); - - // Ambient occlusion level - } else - if (line.matches( - "[ \t]*const[ \t]*float[ \t]*ambientOcclusionLevel[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER - .debug("AO Level: " + parts[1]); - aoLevel = Float.parseFloat(parts[1]); - blockAoLight = 1.0f - aoLevel; - - // super sampling - } else - if (line.matches( - "[ \t]*const[ \t]*int[ \t]*superSamplingLevel[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - int ssaa = Integer.parseInt(parts[1]); - if (ssaa > 1) { - AngelicaTweaker.LOGGER.debug( - "Super sampling level: " + ssaa - + "x"); - superSamplingLevel = ssaa; - } else { - superSamplingLevel = 1; - } - - // noise texture - } else - if (line.matches( - "[ \t]*const[ \t]*int[ \t]*noiseTextureResolution[ \t]*=[ \t]*-?[0-9.]+f?;.*")) { - String[] parts = line.split("(=[ \t]*|;)"); - AngelicaTweaker.LOGGER - .debug("Noise texture enabled"); - AngelicaTweaker.LOGGER.debug( - "Noise texture resolution: " - + parts[1]); - noiseTextureResolution = Integer - .parseInt(parts[1]); - noiseTextureEnabled = true; - - } else - if (line.matches( - "[ \t]*const[ \t]*int[ \t]*\\w+Format[ \t]*=[ \t]*[RGBA81632F]*[ \t]*;.*")) { - Matcher m = gbufferFormatPattern - .matcher(line); - m.matches(); - String name = m.group(1); - String value = m.group(2); - int bufferindex = getBufferIndexFromString( - name); - int format = getTextureFormatFromString( - value); - if (bufferindex >= 0 && format != 0) { - gbuffersFormat[bufferindex] = format; - AngelicaTweaker.LOGGER.debug( - "{} format: {}", - name, - value); - } - // gaux4 - } else - if (line.matches( - "/\\* GAUX4FORMAT:RGBA32F \\*/.*")) { - AngelicaTweaker.LOGGER.debug( - "gaux4 format : RGB32AF"); - gbuffersFormat[7] = GL_RGBA32F; - } else - if (line.matches( - "/\\* GAUX4FORMAT:RGB32F \\*/.*")) { - AngelicaTweaker.LOGGER.debug( - "gaux4 format : RGB32F"); - gbuffersFormat[7] = GL_RGB32F; - } else - if (line.matches( - "/\\* GAUX4FORMAT:RGB16 \\*/.*")) { - AngelicaTweaker.LOGGER - .debug( - "gaux4 format : RGB16"); - gbuffersFormat[7] = GL_RGB16; - - // Mipmap stuff - } else - if (line.matches( - "[ \t]*const[ \t]*bool[ \t]*\\w+MipmapEnabled[ \t]*=[ \t]*true[ \t]*;.*")) { - if (filename.matches( - ".*composite[0-9]?.fsh") - || filename - .matches( - ".*final.fsh")) { - Matcher m = gbufferMipmapEnabledPattern - .matcher( - line); - m.matches(); - String name = m - .group(1); - // String value - // =m.group(2); - int bufferindex = getBufferIndexFromString( - name); - if (bufferindex - >= 0) { - newCompositeMipmapSetting |= (1 - << bufferindex); - AngelicaTweaker.LOGGER - .debug( - "{} mipmap enabled for {}", - name, - filename); - } - } - } else - if (line.matches( - "/\\* DRAWBUFFERS:[0-7N]* \\*/.*")) { - String[] parts = line - .split( - "(:| )", - 4); - newDrawBufSetting = parts[2]; - } - } - - private static boolean printLogInfo(int obj, String name) { - IntBuffer iVal = BufferUtils.createIntBuffer(1); - glGetObjectParameterARB(obj, GL_OBJECT_INFO_LOG_LENGTH_ARB, iVal); - - int length = iVal.get(); - if (length > 1) { - ByteBuffer infoLog = BufferUtils.createByteBuffer(length); - iVal.flip(); - glGetInfoLogARB(obj, iVal, infoLog); - byte[] infoBytes = new byte[length]; - infoLog.get(infoBytes); - if (infoBytes[length - 1] == 0) infoBytes[length - 1] = 10; - String out = new String(infoBytes); - AngelicaTweaker.LOGGER.info("Info log: " + name + "\n" + out); - return false; - } - return true; - } - - public static void setDrawBuffers(IntBuffer drawBuffers) { - if (activeDrawBuffers != drawBuffers) { - // printIntBuffer("setDrawBuffers", drawBuffers); - activeDrawBuffers = drawBuffers; - glDrawBuffers(drawBuffers); - } else { - // printIntBuffer("setDrawBf skip", drawBuffers); - } - } - - public static void useProgram(int program) { - // System.out.println((new StringBuilder(32)).append("useProgram ").append(programNames[program])); - Shaders.checkGLError("pre-useProgram"); - if (isShadowPass) { - program = ProgramShadow; - if (programsID[ProgramShadow] == 0) { - normalMapEnabled = false; - return; - } - } - if (activeProgram == program) { - return; - } - activeProgram = program; - glUseProgramObjectARB(programsID[program]); - if (programsID[program] == 0) { - normalMapEnabled = false; - return; - } - if (checkGLError("useProgram ", programNames[program]) != 0) { - programsID[program] = 0; - } - IntBuffer drawBuffers = programsDrawBuffers[program]; - if (isRenderingDfb) { - setDrawBuffers(drawBuffers); - checkGLError(programNames[program], " draw buffers = ", programsDrawBufSettings[program]); - // ALog.info("%s",programNames[program] + " draw buffers = " + programsDrawBufSettings[program]); - } - activeCompositeMipmapSetting = programsCompositeMipmapSetting[program]; - uniformEntityHurt = glGetUniformLocationARB(programsID[activeProgram], "entityHurt"); - uniformEntityFlash = glGetUniformLocationARB(programsID[activeProgram], "entityFlash"); - switch (program) { - case ProgramBasic: - case ProgramTextured: - case ProgramTexturedLit: - case ProgramSkyBasic: - case ProgramSkyTextured: - case ProgramTerrain: - case ProgramWater: - case ProgramEntities: - case ProgramHand: - case ProgramWeather: - normalMapEnabled = true; - setProgramUniform1i("texture", 0); - setProgramUniform1i("lightmap", 1); - setProgramUniform1i("normals", 2); - setProgramUniform1i("specular", 3); - setProgramUniform1i("shadow", waterShadowEnabled ? 5 : 4); - setProgramUniform1i("watershadow", 4); - setProgramUniform1i("shadowtex0", 4); - setProgramUniform1i("shadowtex1", 5); - setProgramUniform1i("depthtex0", 6); - setProgramUniform1i("depthtex1", 12); - setProgramUniform1i("shadowcolor", 13); - setProgramUniform1i("shadowcolor0", 13); - setProgramUniform1i("shadowcolor1", 14); - setProgramUniform1i("noisetex", 15); - break; - case ProgramComposite: - case ProgramComposite1: - case ProgramComposite2: - case ProgramComposite3: - case ProgramComposite4: - case ProgramComposite5: - case ProgramComposite6: - case ProgramComposite7: - case ProgramFinal: - normalMapEnabled = false; - setProgramUniform1i("gcolor", 0); - setProgramUniform1i("gdepth", 1); - setProgramUniform1i("gnormal", 2); - setProgramUniform1i("composite", 3); - setProgramUniform1i("gaux1", 7); - setProgramUniform1i("gaux2", 8); - setProgramUniform1i("gaux3", 9); - setProgramUniform1i("gaux4", 10); - setProgramUniform1i("colortex0", 0); - setProgramUniform1i("colortex1", 1); - setProgramUniform1i("colortex2", 2); - setProgramUniform1i("colortex3", 3); - setProgramUniform1i("colortex4", 7); - setProgramUniform1i("colortex5", 8); - setProgramUniform1i("colortex6", 9); - setProgramUniform1i("colortex7", 10); - setProgramUniform1i("shadow", waterShadowEnabled ? 5 : 4); - setProgramUniform1i("watershadow", 4); - setProgramUniform1i("shadowtex0", 4); - setProgramUniform1i("shadowtex1", 5); - setProgramUniform1i("gdepthtex", 6); - setProgramUniform1i("depthtex0", 6); - setProgramUniform1i("depthtex1", 11); - setProgramUniform1i("depthtex2", 12); - setProgramUniform1i("shadowcolor", 13); - setProgramUniform1i("shadowcolor0", 13); - setProgramUniform1i("shadowcolor1", 14); - setProgramUniform1i("noisetex", 15); - break; - case ProgramShadow: - setProgramUniform1i("tex", 0); - setProgramUniform1i("texture", 0); - setProgramUniform1i("lightmap", 1); - setProgramUniform1i("normals", 2); - setProgramUniform1i("specular", 3); - setProgramUniform1i("shadow", waterShadowEnabled ? 5 : 4); - setProgramUniform1i("watershadow", 4); - setProgramUniform1i("shadowtex0", 4); - setProgramUniform1i("shadowtex1", 5); - setProgramUniform1i("shadowcolor", 13); - setProgramUniform1i("shadowcolor0", 13); - setProgramUniform1i("shadowcolor1", 14); - setProgramUniform1i("noisetex", 15); - break; - default: - normalMapEnabled = false; - break; - } - ItemStack stack = mc.thePlayer.getCurrentEquippedItem(); - Item item = (stack != null) ? stack.getItem() : null; - int itemID; - Block block; - if (item != null) { - itemID = Item.itemRegistry.getIDForObject(item); - block = (Block) Block.blockRegistry.getObjectById(itemID); - } else { - itemID = -1; - block = null; - } - setProgramUniform1i("heldItemId", (itemID)); - setProgramUniform1i("heldBlockLightValue", (block != null) ? block.getLightValue() : 0); - setProgramUniform1i("fogMode", (fogEnabled ? fogMode : 0)); - setProgramUniform3f("fogColor", fogColorR, fogColorG, fogColorB); - setProgramUniform3f("skyColor", skyColorR, skyColorG, skyColorB); - setProgramUniform1i("worldTime", (int) worldTime % 24000); - setProgramUniform1i("moonPhase", moonPhase); - setProgramUniform1f("frameTimeCounter", frameTimeCounter); - setProgramUniform1f("sunAngle", sunAngle); - setProgramUniform1f("shadowAngle", shadowAngle); - setProgramUniform1f("rainStrength", rainStrength); - setProgramUniform1f("aspectRatio", (float) renderWidth / (float) renderHeight); - setProgramUniform1f("viewWidth", (float) renderWidth); - setProgramUniform1f("viewHeight", (float) renderHeight); - setProgramUniform1f("near", 0.05F); - setProgramUniform1f("far", mc.gameSettings.renderDistanceChunks * 16); - setProgramUniform3f("sunPosition", sunPosition[0], sunPosition[1], sunPosition[2]); - setProgramUniform3f("moonPosition", moonPosition[0], moonPosition[1], moonPosition[2]); - setProgramUniform3f("upPosition", upPosition[0], upPosition[1], upPosition[2]); - setProgramUniform3f( - "previousCameraPosition", - (float) previousCameraPosition[0], - (float) previousCameraPosition[1], - (float) previousCameraPosition[2]); - setProgramUniform3f( - "cameraPosition", - (float) cameraPosition[0], - (float) cameraPosition[1], - (float) cameraPosition[2]); - setProgramUniformMatrix4ARB("gbufferModelView", false, modelView); - setProgramUniformMatrix4ARB("gbufferModelViewInverse", false, modelViewInverse); - setProgramUniformMatrix4ARB("gbufferPreviousProjection", false, previousProjection); - setProgramUniformMatrix4ARB("gbufferProjection", false, projection); - setProgramUniformMatrix4ARB("gbufferProjectionInverse", false, projectionInverse); - setProgramUniformMatrix4ARB("gbufferPreviousModelView", false, previousModelView); - if (usedShadowDepthBuffers > 0) { - setProgramUniformMatrix4ARB("shadowProjection", false, shadowProjection); - setProgramUniformMatrix4ARB("shadowProjectionInverse", false, shadowProjectionInverse); - setProgramUniformMatrix4ARB("shadowModelView", false, shadowModelView); - setProgramUniformMatrix4ARB("shadowModelViewInverse", false, shadowModelViewInverse); - } - setProgramUniform1f("wetness", wetness); - setProgramUniform1f("eyeAltitude", eyePosY); - // .x = block brightness .y = sky brightness - // value : 0,16,32,64,96,112,128,160,192,208,224,240 for light level 0 to 15 - setProgramUniform2i("eyeBrightness", (eyeBrightness & 0xffff), (eyeBrightness >> 16)); - setProgramUniform2i("eyeBrightnessSmooth", Math.round(eyeBrightnessFadeX), Math.round(eyeBrightnessFadeY)); - setProgramUniform2i("terrainTextureSize", terrainTextureSize[0], terrainTextureSize[1]); - setProgramUniform1i("terrainIconSize", terrainIconSize); - setProgramUniform1i("isEyeInWater", isEyeInWater); - setProgramUniform1i("hideGUI", mc.gameSettings.hideGUI ? 1 : 0); - setProgramUniform1f("centerDepthSmooth", centerDepthSmooth); - setProgramUniform2i("atlasSize", atlasSizeX, atlasSizeY); - Shaders.checkGLError("useProgram ", programNames[program]); - } - - public static void setProgramUniform1i(String name, int x) { - int gp = programsID[activeProgram]; - if (gp != GL_NONE) { - int uniform = glGetUniformLocationARB(gp, name); - glUniform1iARB(uniform, x); - if (enableShadersDebug) checkGLError(programNames[activeProgram], name); - } - } - - public static void setProgramUniform2i(String name, int x, int y) { - int gp = programsID[activeProgram]; - if (gp != GL_NONE) { - int uniform = glGetUniformLocationARB(gp, name); - glUniform2iARB(uniform, x, y); - if (enableShadersDebug) checkGLError(programNames[activeProgram], name); - } - } - - public static void setProgramUniform1f(String name, float x) { - int gp = programsID[activeProgram]; - if (gp != GL_NONE) { - int uniform = glGetUniformLocationARB(gp, name); - glUniform1fARB(uniform, x); - if (enableShadersDebug) checkGLError(programNames[activeProgram], name); - } - } - - public static void setProgramUniform3f(String name, float x, float y, float z) { - int gp = programsID[activeProgram]; - if (gp != GL_NONE) { - int uniform = glGetUniformLocationARB(gp, name); - glUniform3fARB(uniform, x, y, z); - if (enableShadersDebug) checkGLError(programNames[activeProgram], name); - } - } - - public static void setProgramUniformMatrix4ARB(String name, boolean transpose, FloatBuffer matrix) { - int gp = programsID[activeProgram]; - if (gp != GL_NONE && matrix != null) { - int uniform = glGetUniformLocationARB(gp, name); - glUniformMatrix4ARB(uniform, transpose, matrix); - if (enableShadersDebug) checkGLError(programNames[activeProgram], name); - } - } - - private static int getBufferIndexFromString(String name) { - if (name.equals("colortex0") || name.equals("gcolor")) return 0; - else if (name.equals("colortex1") || name.equals("gdepth")) return 1; - else if (name.equals("colortex2") || name.equals("gnormal")) return 2; - else if (name.equals("colortex3") || name.equals("composite")) return 3; - else if (name.equals("colortex4") || name.equals("gaux1")) return 4; - else if (name.equals("colortex5") || name.equals("gaux2")) return 5; - else if (name.equals("colortex6") || name.equals("gaux3")) return 6; - else if (name.equals("colortex7") || name.equals("gaux4")) return 7; - else return -1; - } - - private static int getTextureFormatFromString(String par) { - if (par.matches("[ \t]*R8[ \t]*")) return GL_R8; - else if (par.matches("[ \t]*RG8[ \t]*")) return GL_RG8; - else if (par.matches("[ \t]*RGB8[ \t]*")) return GL_RGB8; - else if (par.matches("[ \t]*RGBA8[ \t]*")) return GL_RGBA8; - else if (par.matches("[ \t]*R16[ \t]*")) return GL_R16; - else if (par.matches("[ \t]*RG16[ \t]*")) return GL_RG16; - else if (par.matches("[ \t]*RGB16[ \t]*")) return GL_RGB16; - else if (par.matches("[ \t]*RGBA16[ \t]*")) return GL_RGBA16; - else if (par.matches("[ \t]*R32F[ \t]*")) return GL_R32F; - else if (par.matches("[ \t]*RG32F[ \t]*")) return GL_RG32F; - else if (par.matches("[ \t]*RGB32F[ \t]*")) return GL_RGB32F; - else if (par.matches("[ \t]*RGBA32F[ \t]*")) return GL_RGBA32F; - else return 0; - } - - private static void setupNoiseTexture() { - if (noiseTexture == null) noiseTexture = new HFNoiseTexture(noiseTextureResolution, noiseTextureResolution); - } - - private static IntBuffer fillIntBufferZero(IntBuffer buf) { - int limit = buf.limit(); - for (int i = buf.position(); i < limit; ++i) buf.put(i, 0); - return buf; - } - - public static void uninit() { - if (isInitialized) { - checkGLError("Shaders.uninit pre"); - for (int i = 0; i < ProgramCount; ++i) { - if (programsRef[i] != 0) { - glDeleteObjectARB(programsRef[i]); - checkGLError("del programRef"); - } - programsRef[i] = 0; - programsID[i] = 0; - programsDrawBufSettings[i] = null; - programsDrawBuffers[i] = null; - programsCompositeMipmapSetting[i] = 0; - } - if (dfb != 0) { - glDeleteFramebuffersEXT(dfb); - dfb = 0; - checkGLError("del dfb"); - } - if (sfb != 0) { - glDeleteFramebuffersEXT(sfb); - sfb = 0; - checkGLError("del sfb"); - } - // glDeleteRenderbuffersEXT(dfbDepthBuffer); dfbDepthBuffer = 0; - // if (dfbRenderBuffers != null) { - // glDeleteRenderbuffersEXT(dfbRenderBuffers); - // fillIntBufferZero(dfbRenderBuffers); - // } - if (dfbDepthTextures != null) { - glDeleteTextures(dfbDepthTextures); - fillIntBufferZero(dfbDepthTextures); - checkGLError("del dfbDepthTextures"); - } - if (dfbColorTextures != null) { - glDeleteTextures(dfbColorTextures); - fillIntBufferZero(dfbColorTextures); - checkGLError("del dfbTextures"); - } - if (sfbDepthTextures != null) { - glDeleteTextures(sfbDepthTextures); - fillIntBufferZero(sfbDepthTextures); - checkGLError("del shadow depth"); - } - if (sfbColorTextures != null) { - glDeleteTextures(sfbColorTextures); - fillIntBufferZero(sfbColorTextures); - checkGLError("del shadow color"); - } - if (dfbDrawBuffers != null) { - fillIntBufferZero(dfbDrawBuffers); - } - if (noiseTexture != null) { - noiseTexture.destroy(); - noiseTexture = null; - } - - AngelicaTweaker.LOGGER.trace("UNINIT"); - - shadowPassInterval = 0; - shouldSkipDefaultShadow = false; - isInitialized = false; - notFirstInit = true; - checkGLError("Shaders.uninit"); - } - } - - public static void scheduleResize() { - renderDisplayHeight = 0; - } - - public static void scheduleResizeShadow() { - needResizeShadow = true; - } - - private static void resize() { - renderDisplayWidth = mc.displayWidth; - renderDisplayHeight = mc.displayHeight; - // renderWidth = mc.displayWidth * superSamplingLevel; - // renderHeight = mc.displayHeight * superSamplingLevel; - renderWidth = Math.round(renderDisplayWidth * configRenderResMul); - renderHeight = Math.round(renderDisplayHeight * configRenderResMul); - setupFrameBuffer(); - } - - private static void resizeShadow() { - needResizeShadow = false; - shadowMapWidth = Math.round(spShadowMapWidth * configShadowResMul); - shadowMapHeight = Math.round(spShadowMapHeight * configShadowResMul); - setupShadowFrameBuffer(); - } - - private static void setupFrameBuffer() { - if (dfb != 0) { - glDeleteFramebuffersEXT(dfb); - glDeleteTextures(dfbDepthTextures); - glDeleteTextures(dfbColorTextures); - } - - dfb = glGenFramebuffersEXT(); - glGenTextures((IntBuffer) dfbDepthTextures.clear().limit(usedDepthBuffers)); - glGenTextures((IntBuffer) dfbColorTextures.clear().limit(usedColorBuffers)); - dfbDepthTextures.position(0); - dfbColorTextures.position(0); - - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, dfb); - glDrawBuffers(GL_NONE); - glReadBuffer(GL_NONE); - - for (int i = 0; i < usedDepthBuffers; ++i) { - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(i)); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_DEPTH_COMPONENT, - renderWidth, - renderHeight, - 0, - GL_DEPTH_COMPONENT, - GL11.GL_FLOAT, - (ByteBuffer) null); - } - - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_DEPTH_ATTACHMENT_EXT, - GL_TEXTURE_2D, - dfbDepthTextures.get(0), - 0); - glDrawBuffers(dfbDrawBuffers); - glReadBuffer(GL_NONE); - checkGLError("FT d"); - - for (int i = 0; i < usedColorBuffers; ++i) { - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(i)); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexImage2D( - GL_TEXTURE_2D, - 0, - gbuffersFormat[i], - renderWidth, - renderHeight, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - (ByteBuffer) null); - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_COLOR_ATTACHMENT0_EXT + i, - GL_TEXTURE_2D, - dfbColorTextures.get(i), - 0); - checkGLError("FT c"); - } - - int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - - if (status == GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT) { - printChatAndLogError("Failed using multiple internal formats in frame buffer."); - for (int i = 0; i < usedColorBuffers; ++i) { - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(i)); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RGBA, - renderWidth, - renderHeight, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - (ByteBuffer) null); - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_COLOR_ATTACHMENT0_EXT + i, - GL_TEXTURE_2D, - dfbColorTextures.get(i), - 0); - checkGLError("FT c"); - } - status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - if (status == GL_FRAMEBUFFER_COMPLETE_EXT) { - printChatAndLogError("Please update graphics driver."); - } - } - - glBindTexture(GL_TEXTURE_2D, 0); - - if (status != GL_FRAMEBUFFER_COMPLETE_EXT) { - printChatAndLogError("Failed creating framebuffer! (Status " + status + ")"); - } else { - AngelicaTweaker.LOGGER.debug("Framebuffer created."); - } - } - - private static void setupShadowFrameBuffer() { - if (usedShadowDepthBuffers == 0) { - return; - } - - if (sfb != 0) { - glDeleteFramebuffersEXT(sfb); - glDeleteTextures(sfbDepthTextures); - glDeleteTextures(sfbColorTextures); - } - - sfb = glGenFramebuffersEXT(); - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, sfb); - glDrawBuffers(GL_NONE); - glReadBuffer(GL_NONE); - - glGenTextures((IntBuffer) sfbDepthTextures.clear().limit(usedShadowDepthBuffers)); - glGenTextures((IntBuffer) sfbColorTextures.clear().limit(usedShadowColorBuffers)); - // printIntBuffer(sfbDepthTextures); - // printIntBuffer(sfbColorTextures); - sfbDepthTextures.position(0); - sfbColorTextures.position(0); - - // depth - for (int i = 0; i < usedShadowDepthBuffers; ++i) { - glBindTexture(GL_TEXTURE_2D, sfbDepthTextures.get(i)); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - int filter = shadowFilterNearest[i] ? GL_NEAREST : GL_LINEAR; - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); - if (shadowHardwareFilteringEnabled[i]) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_DEPTH_COMPONENT, - shadowMapWidth, - shadowMapHeight, - 0, - GL_DEPTH_COMPONENT, - GL11.GL_FLOAT, - (FloatBuffer) null); - } - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_DEPTH_ATTACHMENT_EXT, - GL_TEXTURE_2D, - sfbDepthTextures.get(0), - 0); - checkGLError("FT sd"); - - // color shadow - for (int i = 0; i < usedShadowColorBuffers; ++i) { - glBindTexture(GL_TEXTURE_2D, sfbColorTextures.get(i)); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - int filter = shadowColorFilterNearest[i] ? GL_NEAREST : GL_LINEAR; - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RGBA, - shadowMapWidth, - shadowMapHeight, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - (ByteBuffer) null); - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_COLOR_ATTACHMENT0_EXT + i, - GL_TEXTURE_2D, - sfbColorTextures.get(i), - 0); - checkGLError("FT sc"); - } - - glBindTexture(GL_TEXTURE_2D, 0); - - int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - if (status != GL_FRAMEBUFFER_COMPLETE_EXT) { - printChatAndLogError("Failed creating shadow framebuffer! (Status " + status + ")"); - } else { - AngelicaTweaker.LOGGER.debug("Shadow framebuffer created."); - } - } - - /* - * public static void linkTextureNormalMap(int tex0, int tex2, int tex3) { textureIdMap_n.put(tex0, tex2); - * textureIdMap_s.put(tex0, tex3); int[] ast0,ast2,ast3; if (tex0 < Tessellator.atlasSubTextures.length && (ast0 = - * Tessellator.atlasSubTextures[tex0]) != null) { if (tex2 < Tessellator.atlasSubTextures.length && (ast2 = - * Tessellator.atlasSubTextures[tex2]) != null) for (int it = 0, size = Math.min(ast0.length,ast2.length); it < - * size; ++it) textureIdMap_n.put(ast0[it], ast2[it]); if (tex3 < Tessellator.atlasSubTextures.length && (ast3 = - * Tessellator.atlasSubTextures[tex3]) != null) for (int it = 0, size = Math.min(ast0.length,ast3.length); it < - * size; ++it) textureIdMap_s.put(ast0[it], ast3[it]); } } - */ - - public static void beginRender(Minecraft minecraft, float f, long l) { - if (isShadowPass) { - return; - } - checkGLError("pre beginRender"); - - mc = minecraft; - mc.mcProfiler.startSection("init"); - - if (!isInitialized) { - init(); - } - if (mc.displayWidth != renderDisplayWidth || mc.displayHeight != renderDisplayHeight) { - resize(); - } - if (needResizeShadow) { - resizeShadow(); - } - - worldTime = mc.theWorld.getWorldTime(); - diffWorldTime = (worldTime - lastWorldTime) % 24000; - if (diffWorldTime < 0) diffWorldTime += 24000; - lastWorldTime = worldTime; - moonPhase = mc.theWorld.getMoonPhase(); - - systemTime = System.currentTimeMillis(); - if (lastSystemTime == 0) { - lastSystemTime = systemTime; // Initialize lastSystemTime on the first tick so that it is equal to current - // system - // time - } - diffSystemTime = systemTime - lastSystemTime; - lastSystemTime = systemTime; - - frameTimeCounter += diffSystemTime * 0.001f; - frameTimeCounter = frameTimeCounter % 100000.0f; - - rainStrength = minecraft.theWorld.getRainStrength(f); - { - float fadeScalar = diffSystemTime * 0.01f; - // float temp1 = (float)Math.exp(Math.log(0.5)*diffWorldTime/((wetness < rainStrength)? drynessHalfLife : - // wetnessHalfLife)); - float temp1 = (float) Math - .exp(Math.log(0.5) * fadeScalar / ((wetness < rainStrength) ? drynessHalfLife : wetnessHalfLife)); - wetness = wetness * (temp1) + rainStrength * (1 - temp1); - } - - EntityLivingBase eye = mc.renderViewEntity; - eyePosY = (float) eye.posY * f + (float) eye.lastTickPosY * (1 - f); - eyeBrightness = eye.getBrightnessForRender(f); - { - // float temp2 = (float)Math.exp(Math.log(0.5)*1.0f/eyeBrightnessHalfLife); - float fadeScalar = diffSystemTime * 0.01f; - float temp2 = (float) Math.exp(Math.log(0.5) * fadeScalar / eyeBrightnessHalflife); - eyeBrightnessFadeX = eyeBrightnessFadeX * temp2 + (eyeBrightness & 0xffff) * (1 - temp2); - eyeBrightnessFadeY = eyeBrightnessFadeY * temp2 + (eyeBrightness >> 16) * (1 - temp2); - } - - isEyeInWater = (mc.gameSettings.thirdPersonView == 0 && !mc.renderViewEntity.isPlayerSleeping() - && mc.thePlayer.isInsideOfMaterial(Material.water)) ? 1 : 0; - - { - Vec3 skyColorV = mc.theWorld.getSkyColor(mc.renderViewEntity, f); - skyColorR = (float) skyColorV.xCoord; - skyColorG = (float) skyColorV.yCoord; - skyColorB = (float) skyColorV.zCoord; - } - - // Determine average color - // { - // int searchWidth = renderWidth / 2; - // int searchHeight = renderHeight / 2; - // - // ByteBuffer colorByteBuffer = ByteBuffer.allocateDirect(searchWidth * searchHeight * 4 * 3); - // glReadPixels((renderWidth / 2) - (searchWidth / 2), (renderHeight / 2) - (searchHeight / 2), searchWidth, - // searchHeight, GL11.GL_RGB, GL11.GL_FLOAT, colorByteBuffer); - // - // colorByteBuffer.order(ByteOrder.LITTLE_ENDIAN); - // - // - // float averageColor = 0.0f; - // for (int i = 0; i < searchWidth; i++) { - // for (int j = 0; j < searchHeight; j++) { - // averageColor += colorByteBuffer.getFloat(i * 4 + (searchWidth * 4 * 3 * j)); - // } - // } - // - // System.out.println("Color: " + averageColor / (searchWidth * searchHeight)); - // - // } - - isRenderingWorld = true; - isCompositeRendered = false; - isHandRendered = false; - - if (usedShadowDepthBuffers >= 1) { - glActiveTexture(GL_TEXTURE4); - glBindTexture(GL_TEXTURE_2D, sfbDepthTextures.get(0)); - if (usedShadowDepthBuffers >= 2) { - glActiveTexture(GL_TEXTURE5); - glBindTexture(GL_TEXTURE_2D, sfbDepthTextures.get(1)); - } - } - - for (int i = 0; i < 4 && 4 + i < usedColorBuffers; ++i) { - glActiveTexture(GL_TEXTURE7 + i); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(4 + i)); - } - - glActiveTexture(GL_TEXTURE6); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(0)); - - if (usedDepthBuffers >= 2) { - glActiveTexture(GL_TEXTURE11); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(1)); - - if (usedDepthBuffers >= 3) { - glActiveTexture(GL_TEXTURE12); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(2)); - } - } - - for (int i = 0; i < usedShadowColorBuffers; ++i) { - glActiveTexture(GL_TEXTURE13 + i); - glBindTexture(GL_TEXTURE_2D, sfbColorTextures.get(i)); - } - - if (noiseTextureEnabled) { - glActiveTexture(GL_TEXTURE0 + noiseTexture.textureUnit); - glBindTexture(GL_TEXTURE_2D, noiseTexture.getID()); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - } - - glActiveTexture(GL_TEXTURE0); - - previousCameraPosition[0] = cameraPosition[0]; - previousCameraPosition[1] = cameraPosition[1]; - previousCameraPosition[2] = cameraPosition[2]; - - previousProjection.position(0); - projection.position(0); - previousProjection.put(projection); - previousProjection.position(0); - projection.position(0); - previousModelView.position(0); - modelView.position(0); - previousModelView.put(modelView); - previousModelView.position(0); - modelView.position(0); - - EntityRenderer.anaglyphField = 0; - - if (usedShadowDepthBuffers > 0 && --shadowPassCounter <= 0) { - // do shadow pass - // System.out.println("shadow pass start"); - mc.mcProfiler.endStartSection("shadow pass"); - preShadowPassThirdPersonView = mc.gameSettings.thirdPersonView; - boolean preShadowPassAdvancedOpengl = mc.gameSettings.advancedOpengl; - mc.gameSettings.advancedOpengl = false; - - // moved to setCamera - // mc.gameSettings.thirdPersonView = 1; - - isShadowPass = true; - shadowPassCounter = shadowPassInterval; - - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, sfb); - glDrawBuffers(programsDrawBuffers[ProgramShadow]); - - useProgram(ProgramShadow); - - mc.entityRenderer.renderWorld(f, l); - - glFlush(); - - isShadowPass = false; - - mc.gameSettings.advancedOpengl = preShadowPassAdvancedOpengl; - mc.gameSettings.thirdPersonView = preShadowPassThirdPersonView; - - if (hasGlGenMipmap) { - if (usedShadowDepthBuffers >= 1) { - if (shadowMipmapEnabled[0]) { - glActiveTexture(GL_TEXTURE4); - glGenerateMipmap(GL_TEXTURE_2D); - glTexParameteri( - GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - shadowFilterNearest[0] ? GL_NEAREST_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR); - } - if (usedShadowDepthBuffers >= 2) { - if (shadowMipmapEnabled[1]) { - glActiveTexture(GL_TEXTURE5); - glGenerateMipmap(GL_TEXTURE_2D); - glTexParameteri( - GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - shadowFilterNearest[1] ? GL_NEAREST_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR); - } - } - glActiveTexture(GL_TEXTURE0); - } - if (usedShadowColorBuffers >= 1) { - if (shadowColorMipmapEnabled[0]) { - glActiveTexture(GL_TEXTURE13); - glGenerateMipmap(GL_TEXTURE_2D); - glTexParameteri( - GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - shadowColorFilterNearest[0] ? GL_NEAREST_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR); - } - if (usedShadowColorBuffers >= 2) { - if (shadowColorMipmapEnabled[1]) { - glActiveTexture(GL_TEXTURE14); - glGenerateMipmap(GL_TEXTURE_2D); - glTexParameteri( - GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - shadowColorFilterNearest[1] ? GL_NEAREST_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR); - } - } - glActiveTexture(GL_TEXTURE0); - } - } - - // System.out.println("shadow pass end"); - } - mc.mcProfiler.endSection(); - - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, dfb); - GL11.glViewport(0, 0, Shaders.renderWidth, Shaders.renderHeight); - activeDrawBuffers = null; - ShadersTex.bindNSTextures(defaultTexture.angelica$getMultiTexID()); - useProgram(ProgramTextured); - - checkGLError("end beginRender"); - } - - public static void setViewport(int vx, int vy, int vw, int vh) { - GL11.glColorMask(true, true, true, true); - if (isShadowPass) { - GL11.glViewport(0, 0, shadowMapWidth, shadowMapHeight); - } else { - GL11.glViewport(0, 0, Shaders.renderWidth, Shaders.renderHeight); - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, dfb); - isRenderingDfb = true; - GL11.glEnable(GL11.GL_CULL_FACE); - GL11.glEnable(GL11.GL_DEPTH_TEST); - setDrawBuffers(drawBuffersNone); - useProgram(ProgramTextured); - checkGLError("beginRenderPass"); - } - } - - public static int setFogMode(int val) { - return fogMode = val; - } - - public static void setFogColor(float r, float g, float b) { - fogColorR = r; - fogColorG = g; - fogColorB = b; - } - - public static void setClearColor(float red, float green, float blue, float alpha) { - GL11.glClearColor(red, green, blue, alpha); - clearColorR = red; - clearColorG = green; - clearColorB = blue; - } - - public static void clearRenderBuffer() { - // clearColorR = fogColorR; - // clearColorG = fogColorG; - // clearColorB = fogColorB; - - if (isShadowPass) { - // No need to clear shadow depth 1. It will be overwritten. - // if (waterShadowEnabled) { - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, - // sfbDepthTextures.get(1), 0); - // glClear(GL_DEPTH_BUFFER_BIT); - // } - checkGLError("shadow clear pre"); - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_DEPTH_ATTACHMENT_EXT, - GL_TEXTURE_2D, - sfbDepthTextures.get(0), - 0); - glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - // printIntBuffer(programsDrawBuffers[ProgramShadow]); - glDrawBuffers(programsDrawBuffers[ProgramShadow]); - checkFramebufferStatus("shadow clear"); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - checkGLError("shadow clear"); - return; - } - checkGLError("clear pre"); - - /* - * glDrawBuffers(dfbDrawBuffers); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | - * GL_DEPTH_BUFFER_BIT); - */ - - glDrawBuffers(GL_COLOR_ATTACHMENT0_EXT); - // glClearColor(clearColorR, clearColorG, clearColorB, 1.0f); - // glClearColor(1f, 0f, 0f, 1.0f); // for debug - glClear(GL_COLOR_BUFFER_BIT); - - glDrawBuffers(GL_COLOR_ATTACHMENT1_EXT); - glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - for (int i = 2; i < usedColorBuffers; ++i) { - glDrawBuffers(GL_COLOR_ATTACHMENT0_EXT + i); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - } - - setDrawBuffers(dfbDrawBuffers); - checkFramebufferStatus("clear"); - checkGLError("clear"); - } - - public static void setCamera(float f) { - EntityLivingBase viewEntity = mc.renderViewEntity; - - double x = viewEntity.lastTickPosX + (viewEntity.posX - viewEntity.lastTickPosX) * f; - double y = viewEntity.lastTickPosY + (viewEntity.posY - viewEntity.lastTickPosY) * f; - double z = viewEntity.lastTickPosZ + (viewEntity.posZ - viewEntity.lastTickPosZ) * f; - - cameraPosition[0] = x; - cameraPosition[1] = y; - cameraPosition[2] = z; - - glGetFloat(GL_PROJECTION_MATRIX, (FloatBuffer) projection.position(0)); - invertMat4x((FloatBuffer) projection.position(0), (FloatBuffer) projectionInverse.position(0)); - projection.position(0); - projectionInverse.position(0); - - glGetFloat(GL_MODELVIEW_MATRIX, (FloatBuffer) modelView.position(0)); - invertMat4x((FloatBuffer) modelView.position(0), (FloatBuffer) modelViewInverse.position(0)); - modelView.position(0); - modelViewInverse.position(0); - - if (isShadowPass) { - glViewport(0, 0, shadowMapWidth, shadowMapHeight); - - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - - if (shadowMapIsOrtho) { - glOrtho( - -shadowMapHalfPlane, - shadowMapHalfPlane, - -shadowMapHalfPlane, - shadowMapHalfPlane, - 0.05f, - 256.0f); - } else { - // just backwards compatibility. it's only used when SHADOWFOV is set in the shaders. - gluPerspective(shadowMapFOV, (float) shadowMapWidth / (float) shadowMapHeight, 0.05f, 256.0f); - } - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glTranslatef(0.0f, 0.0f, -100.0f); - glRotatef(90.0f, 1.0f, 0.0f, 0.0f); - float celestialAngle = mc.theWorld.getCelestialAngle(f); - sunAngle = (celestialAngle < 0.75f) ? celestialAngle + 0.25f : celestialAngle - 0.75f; - float angle = celestialAngle * (-360.0f); - float angleInterval = shadowAngleInterval > 0.0f - ? (angle % shadowAngleInterval - (shadowAngleInterval * 0.5f)) - : 0.0f; - if (sunAngle <= 0.5) { - // day time - glRotatef(angle - angleInterval, 0.0f, 0.0f, 1.0f); - glRotatef(sunPathRotation, 1.0f, 0.0f, 0.0f); // rotate - shadowAngle = sunAngle; - } else { - // night time - glRotatef(angle + 180.0f - angleInterval, 0.0f, 0.0f, 1.0f); - glRotatef(sunPathRotation, 1.0f, 0.0f, 0.0f); // rotate - shadowAngle = sunAngle - 0.5f; - } - if (shadowMapIsOrtho) { - // reduces jitter - float trans = shadowIntervalSize; - float trans2 = trans / 2.0f; - glTranslatef((float) x % trans - trans2, (float) y % trans - trans2, (float) z % trans - trans2); - } - - glGetFloat(GL_PROJECTION_MATRIX, (FloatBuffer) shadowProjection.position(0)); - invertMat4x((FloatBuffer) shadowProjection.position(0), (FloatBuffer) shadowProjectionInverse.position(0)); - shadowProjection.position(0); - shadowProjectionInverse.position(0); - - glGetFloat(GL_MODELVIEW_MATRIX, (FloatBuffer) shadowModelView.position(0)); - invertMat4x((FloatBuffer) shadowModelView.position(0), (FloatBuffer) shadowModelViewInverse.position(0)); - shadowModelView.position(0); - shadowModelViewInverse.position(0); - - setProgramUniformMatrix4ARB("gbufferProjection", false, projection); - setProgramUniformMatrix4ARB("gbufferProjectionInverse", false, projectionInverse); - setProgramUniformMatrix4ARB("gbufferPreviousProjection", false, previousProjection); - setProgramUniformMatrix4ARB("gbufferModelView", false, modelView); - setProgramUniformMatrix4ARB("gbufferModelViewInverse", false, modelViewInverse); - setProgramUniformMatrix4ARB("gbufferPreviousModelView", false, previousModelView); - setProgramUniformMatrix4ARB("shadowProjection", false, shadowProjection); - setProgramUniformMatrix4ARB("shadowProjectionInverse", false, shadowProjectionInverse); - setProgramUniformMatrix4ARB("shadowModelView", false, shadowModelView); - setProgramUniformMatrix4ARB("shadowModelViewInverse", false, shadowModelViewInverse); - - // Also render player shadow - mc.gameSettings.thirdPersonView = 1; - checkGLError("setCamera"); - return; - } - checkGLError("setCamera"); - } - - public static void preCelestialRotate() { - Shaders.setUpPosition(); - GL11.glRotatef(Shaders.sunPathRotation * 1.0f, 0.0f, 0.0f, 1.0f); - checkGLError("preCelestialRotate"); - } - - public static void postCelestialRotate() { - // This is called when the current matrix is the modelview matrix based on the celestial angle. - // The sun is at (0, 100, 0), and the moon is at (0, -100, 0). - FloatBuffer modelView = tempMatrixDirectBuffer; - modelView.clear(); - glGetFloat(GL_MODELVIEW_MATRIX, modelView); - modelView.get(tempMat, 0, 16); - multiplyMat4xVec4(tempMat, sunPosModelView, sunPosition); - multiplyMat4xVec4(tempMat, moonPosModelView, moonPosition); - checkGLError("postCelestialRotate"); - } - - public static void setUpPosition() { - // Up direction in model view while rendering sky. - FloatBuffer modelView = tempMatrixDirectBuffer; - modelView.clear(); - glGetFloat(GL_MODELVIEW_MATRIX, modelView); - modelView.get(tempMat, 0, 16); - multiplyMat4xVec4(tempMat, upPosModelView, upPosition); - } - - private static float[] multiplyMat4xVec4(float[] matA, float[] vecB, float[] vecOut) { - vecOut[0] = matA[0] * vecB[0] + matA[4] * vecB[1] + matA[8] * vecB[2] + matA[12] * vecB[3]; - vecOut[1] = matA[1] * vecB[0] + matA[5] * vecB[1] + matA[9] * vecB[2] + matA[13] * vecB[3]; - vecOut[2] = matA[2] * vecB[0] + matA[6] * vecB[1] + matA[10] * vecB[2] + matA[14] * vecB[3]; - vecOut[3] = matA[3] * vecB[0] + matA[7] * vecB[1] + matA[11] * vecB[2] + matA[15] * vecB[3]; - return vecOut; - } - - static float[] invertMat4x_m = new float[16]; - static float[] invertMat4x_inv = new float[16]; - - private static FloatBuffer invertMat4x(FloatBuffer matIn, FloatBuffer invMatOut) { - float[] m = invertMat4x_m; - float[] inv = invertMat4x_inv; - float det; - int i; - - matIn.get(m); - - inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - - m[9] * m[6] * m[15] - + m[9] * m[7] * m[14] - + m[13] * m[6] * m[11] - - m[13] * m[7] * m[10]; - inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] - + m[8] * m[6] * m[15] - - m[8] * m[7] * m[14] - - m[12] * m[6] * m[11] - + m[12] * m[7] * m[10]; - inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - - m[8] * m[5] * m[15] - + m[8] * m[7] * m[13] - + m[12] * m[5] * m[11] - - m[12] * m[7] * m[9]; - inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] - + m[8] * m[5] * m[14] - - m[8] * m[6] * m[13] - - m[12] * m[5] * m[10] - + m[12] * m[6] * m[9]; - inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] - + m[9] * m[2] * m[15] - - m[9] * m[3] * m[14] - - m[13] * m[2] * m[11] - + m[13] * m[3] * m[10]; - inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - - m[8] * m[2] * m[15] - + m[8] * m[3] * m[14] - + m[12] * m[2] * m[11] - - m[12] * m[3] * m[10]; - inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] - + m[8] * m[1] * m[15] - - m[8] * m[3] * m[13] - - m[12] * m[1] * m[11] - + m[12] * m[3] * m[9]; - inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - - m[8] * m[1] * m[14] - + m[8] * m[2] * m[13] - + m[12] * m[1] * m[10] - - m[12] * m[2] * m[9]; - inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - - m[5] * m[2] * m[15] - + m[5] * m[3] * m[14] - + m[13] * m[2] * m[7] - - m[13] * m[3] * m[6]; - inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] - + m[4] * m[2] * m[15] - - m[4] * m[3] * m[14] - - m[12] * m[2] * m[7] - + m[12] * m[3] * m[6]; - inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - - m[4] * m[1] * m[15] - + m[4] * m[3] * m[13] - + m[12] * m[1] * m[7] - - m[12] * m[3] * m[5]; - inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] - + m[4] * m[1] * m[14] - - m[4] * m[2] * m[13] - - m[12] * m[1] * m[6] - + m[12] * m[2] * m[5]; - inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] - + m[5] * m[2] * m[11] - - m[5] * m[3] * m[10] - - m[9] * m[2] * m[7] - + m[9] * m[3] * m[6]; - inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - - m[4] * m[2] * m[11] - + m[4] * m[3] * m[10] - + m[8] * m[2] * m[7] - - m[8] * m[3] * m[6]; - inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] - + m[4] * m[1] * m[11] - - m[4] * m[3] * m[9] - - m[8] * m[1] * m[7] - + m[8] * m[3] * m[5]; - inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - - m[4] * m[1] * m[10] - + m[4] * m[2] * m[9] - + m[8] * m[1] * m[6] - - m[8] * m[2] * m[5]; - - det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; - - if (det != 0.0) { - for (i = 0; i < 16; ++i) inv[i] /= det; - } else { - // no inverse - Arrays.fill(inv, 0); - } - - invMatOut.put(inv); - - return invMatOut; - } - - public static void genCompositeMipmap() { - if (hasGlGenMipmap) { - if ((activeCompositeMipmapSetting & (1 << 0)) != 0) { - glActiveTexture(GL_TEXTURE0); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); - } - - if ((activeCompositeMipmapSetting & (1 << 1)) != 0) { - glActiveTexture(GL_TEXTURE1); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); - } - - if ((activeCompositeMipmapSetting & (1 << 2)) != 0) { - glActiveTexture(GL_TEXTURE2); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); - } - - if ((activeCompositeMipmapSetting & (1 << 3)) != 0) { - glActiveTexture(GL_TEXTURE3); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); - } - - for (int i = 0; i < 4 && 4 + i < usedColorBuffers; ++i) { - if ((activeCompositeMipmapSetting & ((1 << 4) << i)) != 0) { - glActiveTexture(GL_TEXTURE7 + i); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); - } - } - - glActiveTexture(GL_TEXTURE0); - } - } - - public static void drawComposite() { - glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - glBegin(GL_QUADS); - glTexCoord2f(0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glTexCoord2f(1.0f, 0.0f); - glVertex3f(1.0f, 0.0f, 0.0f); - glTexCoord2f(1.0f, 1.0f); - glVertex3f(1.0f, 1.0f, 0.0f); - glTexCoord2f(0.0f, 1.0f); - glVertex3f(0.0f, 1.0f, 0.0f); - glEnd(); - } - - public static void renderCompositeFinal() { - if (isShadowPass) { - // useProgram(ProgramNone); - return; - } - - checkGLError("pre-renderCompositeFinal"); - // fogColorR = clearColorR; - // fogColorG = clearColorG; - // fogColorB = clearColorB; - - // glMatrixMode(GL_MODELVIEW); - glPushMatrix(); - glLoadIdentity(); - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - glLoadIdentity(); - glOrtho(0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f); - - glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - glEnable(GL_TEXTURE_2D); - glDisable(GL_ALPHA_TEST); - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_ALWAYS); - glDepthMask(false); - - // textures - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(0)); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(1)); - - glActiveTexture(GL_TEXTURE2); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(2)); - - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(3)); - - if (usedShadowDepthBuffers >= 1) { - glActiveTexture(GL_TEXTURE4); - glBindTexture(GL_TEXTURE_2D, sfbDepthTextures.get(0)); - if (usedShadowDepthBuffers >= 2) { - glActiveTexture(GL_TEXTURE5); - glBindTexture(GL_TEXTURE_2D, sfbDepthTextures.get(1)); - } - } - - for (int i = 0; i < 4 && 4 + i < usedColorBuffers; ++i) { - glActiveTexture(GL_TEXTURE7 + i); - glBindTexture(GL_TEXTURE_2D, dfbColorTextures.get(4 + i)); - } - - glActiveTexture(GL_TEXTURE6); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(0)); - - if (usedDepthBuffers >= 2) { - glActiveTexture(GL_TEXTURE11); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(1)); - - if (usedDepthBuffers >= 3) { - glActiveTexture(GL_TEXTURE12); - glBindTexture(GL_TEXTURE_2D, dfbDepthTextures.get(2)); - } - } - - for (int i = 0; i < usedShadowColorBuffers; ++i) { - glActiveTexture(GL_TEXTURE13 + i); - glBindTexture(GL_TEXTURE_2D, sfbColorTextures.get(i)); - } - - if (noiseTextureEnabled) { - glActiveTexture(GL_TEXTURE0 + noiseTexture.textureUnit); - glBindTexture(GL_TEXTURE_2D, noiseTexture.getID()); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - } - - glActiveTexture(GL_TEXTURE0); - - // set depth buffer - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, - GL_DEPTH_ATTACHMENT_EXT, - GL_TEXTURE_2D, - dfbDepthTextures.get(0), - 0); - // detach depth buffer - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, 0, 0); - - // composite - glDrawBuffers(dfbDrawBuffers); - checkGLError("pre-composite"); - - for (int i = 0; i < MaxCompositePasses; ++i) { - if (programsID[ProgramComposite + i] != 0) { - useProgram(ProgramComposite + i); - checkGLError(programNames[ProgramComposite + i]); - if (activeCompositeMipmapSetting != 0) genCompositeMipmap(); - drawComposite(); - } - } - - // reattach depth buffer - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, dfbDepthTexture, 0); - - // final render target - isRenderingDfb = false; - // glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); - // GL11.glViewport(0, 0, mc.displayWidth, mc.displayHeight); - mc.getFramebuffer().bindFramebuffer(true); - if (EntityRenderer.anaglyphEnable) { - boolean maskR = (EntityRenderer.anaglyphField != 0); - glColorMask(maskR, !maskR, !maskR, true); - } - glDepthMask(true); - glClearColor(clearColorR, clearColorG, clearColorB, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - glEnable(GL_TEXTURE_2D); - glDisable(GL_ALPHA_TEST); - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_ALWAYS); - glDepthMask(false); - - // final - checkGLError("pre-final"); - useProgram(ProgramFinal); - checkGLError("final"); - if (activeCompositeMipmapSetting != 0) genCompositeMipmap(); - drawComposite(); - - // Read gl_FragColor - // ByteBuffer centerPixelColor = ByteBuffer.allocateDirect(3); - // glReadPixels(renderWidth / 2, renderHeight / 2, 1, 1, GL11.GL_RGB, GL11.GL_BYTE, centerPixelColor); - // System.out.println(centerPixelColor.get(0)); - - // end - checkGLError("renderCompositeFinal"); - isCompositeRendered = true; - glEnable(GL_TEXTURE_2D); - glEnable(GL_ALPHA_TEST); - glEnable(GL_BLEND); - glDepthFunc(GL_LEQUAL); - glDepthMask(true); - - // glMatrixMode(GL_PROJECTION); - glPopMatrix(); - glMatrixMode(GL_MODELVIEW); - glPopMatrix(); - - useProgram(ProgramNone); - } - - public static void endRender() { - if (isShadowPass) { - // useProgram(ProgramNone); - checkGLError("shadow endRender"); - return; - } - - if (!isCompositeRendered) renderCompositeFinal(); - isRenderingWorld = false; - - glColorMask(true, true, true, true); - glEnable(GL_BLEND); - useProgram(ProgramNone); - checkGLError("endRender end"); - - // defaultTexture.bind(); - } - - public static void beginSky() { - isRenderingSky = true; - fogEnabled = true; - setDrawBuffers(dfbDrawBuffers); - useProgram(ProgramSkyTextured); - pushEntity(-2, 0); - } - - public static void setSkyColor(Vec3 v3color) { - skyColorR = (float) v3color.xCoord; - skyColorG = (float) v3color.yCoord; - skyColorB = (float) v3color.zCoord; - setProgramUniform3f("skyColor", skyColorR, skyColorG, skyColorB); - } - - public static void drawHorizon() { - Tessellator tess = Tessellator.instance; - float farDistance = mc.gameSettings.renderDistanceChunks * 16; - double xzq = farDistance * 0.9238; - double xzp = farDistance * 0.3826; - double xzn = -xzp; - double xzm = -xzq; - double top = 16f; // 256f-cameraPosition[1]; - double bot = -cameraPosition[1]; - - tess.startDrawingQuads(); - // horizon - tess.addVertex(xzn, bot, xzm); - tess.addVertex(xzn, top, xzm); - tess.addVertex(xzm, top, xzn); - tess.addVertex(xzm, bot, xzn); - - tess.addVertex(xzm, bot, xzn); - tess.addVertex(xzm, top, xzn); - tess.addVertex(xzm, top, xzp); - tess.addVertex(xzm, bot, xzp); - - tess.addVertex(xzm, bot, xzp); - tess.addVertex(xzm, top, xzp); - tess.addVertex(xzn, top, xzp); - tess.addVertex(xzn, bot, xzp); - - tess.addVertex(xzn, bot, xzp); - tess.addVertex(xzn, top, xzp); - tess.addVertex(xzp, top, xzq); - tess.addVertex(xzp, bot, xzq); - - tess.addVertex(xzp, bot, xzq); - tess.addVertex(xzp, top, xzq); - tess.addVertex(xzq, top, xzp); - tess.addVertex(xzq, bot, xzp); - - tess.addVertex(xzq, bot, xzp); - tess.addVertex(xzq, top, xzp); - tess.addVertex(xzq, top, xzn); - tess.addVertex(xzq, bot, xzn); - - tess.addVertex(xzq, bot, xzn); - tess.addVertex(xzq, top, xzn); - tess.addVertex(xzp, top, xzm); - tess.addVertex(xzp, bot, xzm); - - tess.addVertex(xzp, bot, xzm); - tess.addVertex(xzp, top, xzm); - tess.addVertex(xzn, top, xzm); - tess.addVertex(xzn, bot, xzm); - // bottom - // tess.addVertex(xzm, bot, xzm); - // tess.addVertex(xzm, bot, xzq); - // tess.addVertex(xzq, bot, xzq); - // tess.addVertex(xzq, bot, xzm); - - tess.draw(); - } - - public static void preSkyList() { - GL11.glColor3f(fogColorR, fogColorG, fogColorB); - // GL11.glColor3f(0f, 1f, 0f); - // glDisable(GL_FOG); - - drawHorizon(); - - // glEnable(GL_FOG); - GL11.glColor3f(skyColorR, skyColorG, skyColorB); - } - - public static void endSky() { - isRenderingSky = false; - setDrawBuffers(dfbDrawBuffers); - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - popEntity(); - } - - // public static void beginSunMoon() { - // useProgram(ProgramNone); - // } - - // public static void endSunMoon() { - // useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - // } - public static void beginUpdateChunks() { - // System.out.println("beginUpdateChunks"); - checkGLError("beginUpdateChunks1"); - checkFramebufferStatus("beginUpdateChunks1"); - if (!isShadowPass) { - useProgram(ProgramTerrain); - } - checkGLError("beginUpdateChunks2"); - checkFramebufferStatus("beginUpdateChunks2"); - } - - public static void endUpdateChunks() { - // System.out.println("endUpdateChunks"); - checkGLError("endUpdateChunks1"); - checkFramebufferStatus("endUpdateChunks1"); - if (!isShadowPass) { - useProgram(ProgramTerrain); - } - checkGLError("endUpdateChunks2"); - checkFramebufferStatus("endUpdateChunks2"); - } - - public static boolean shouldRenderClouds(GameSettings gs) { - checkGLError("shouldRenderClouds"); - return isShadowPass ? configCloudShadow : gs.clouds; - } - - public static void beginClouds() { - fogEnabled = true; - pushEntity(-3, 0); - useProgram(ProgramTextured); - } - - public static void endClouds() { - disableFog(); - popEntity(); - } - - public static void beginTerrain() { - if (isRenderingWorld) { - if (isShadowPass) { - glDisable(GL11.GL_CULL_FACE); - } - fogEnabled = true; - useProgram(Shaders.ProgramTerrain); - // ShadersTex.bindNSTextures(defaultTexture.getMultiTexID()); // flat - } - } - - public static void endTerrain() { - if (isRenderingWorld) { - if (isShadowPass) { - glEnable(GL11.GL_CULL_FACE); - } - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - // ShadersTex.bindNSTextures(defaultTexture.getMultiTexID()); // flat - } - } - - public static void beginBlockEntities() { - if (isRenderingWorld) { - checkGLError("beginBlockEntities"); - useProgram(Shaders.ProgramTerrain); - } - } - - public static void endBlockEntities() { - if (isRenderingWorld) { - checkGLError("endBlockEntities"); - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - ShadersTex.bindNSTextures(defaultTexture.angelica$getMultiTexID()); - } - } - - public static void beginBlockDestroyProgress() { - if (isRenderingWorld) { - useProgram(ProgramTerrain); - if (Shaders.configTweakBlockDamage) { - setDrawBuffers(drawBuffersColorAtt0); - glDepthMask(false); - // GL14.glBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA,GL11.GL_ONE,GL11.GL_ZERO); - } - } - } - - public static void endBlockDestroyProgress() { - if (isRenderingWorld) { - glDepthMask(true); - useProgram(ProgramTexturedLit); - } - } - - public static void beginEntities() { - if (isRenderingWorld) { - useProgram(ProgramEntities); - if (programsID[activeProgram] != 0) { - useEntityHurtFlash = (uniformEntityHurt != -1) || (uniformEntityFlash != -1); - if (uniformEntityHurt != -1) glUniform1iARB(uniformEntityHurt, 0); - if (uniformEntityHurt != -1) glUniform1iARB(uniformEntityFlash, 0); - } else { - useEntityHurtFlash = false; - } - } - } - - public static void nextEntity() { - if (isRenderingWorld) { - useProgram(ProgramEntities); - } - } - - public static void beginSpiderEyes() { - if (isRenderingWorld) { - useProgram(ProgramSpiderEyes); - if (programsID[ProgramSpiderEyes] == programsID[ProgramTextured]) { - GL11.glEnable(GL11.GL_ALPHA_TEST); - GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); - } - } - } - - public static void endEntities() { - if (isRenderingWorld) { - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - } - } - - public static void setEntityHurtFlash(int hurt, int flash) { - if (useEntityHurtFlash && isRenderingWorld && !isShadowPass) { - if (uniformEntityHurt != -1) glUniform1iARB(uniformEntityHurt, hurt); - if (uniformEntityFlash != -1) glUniform1iARB(uniformEntityFlash, flash >> 24); - checkGLError("setEntityHurtFlash"); - } - } - - public static void resetEntityHurtFlash() { - setEntityHurtFlash(0, 0); - } - - public static void beginLivingDamage() { - if (isRenderingWorld) { - ShadersTex.bindTexture(defaultTexture); - if (!isShadowPass) { - // useProgram(ProgramBasic); - setDrawBuffers(drawBuffersColorAtt0); - } - } - } - - public static void endLivingDamage() { - if (isRenderingWorld) { - if (!isShadowPass) { - // useProgram(ProgramEntities); - setDrawBuffers(programsDrawBuffers[ProgramEntities]); - } - } - } - - public static void beginLitParticles() { - // GL11.glDepthMask(false); - Tessellator.instance.setNormal(0f, 0f, 0f); - useProgram(ProgramTexturedLit); - } - - public static void beginParticles() { - // GL11.glDepthMask(false); - Tessellator.instance.setNormal(0f, 0f, 0f); - useProgram(ProgramTextured); - } - - public static void endParticles() { - Tessellator.instance.setNormal(0f, 0f, 0f); - useProgram(ProgramTexturedLit); - } - - public static void preWater() { - if (isShadowPass) { - if (usedShadowDepthBuffers >= 2) { - // copy depth buffer to shadowtex1 - glActiveTexture(GL_TEXTURE5); - checkGLError("pre copy shadow depth"); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, shadowMapWidth, shadowMapHeight); - checkGLError("copy shadow depth"); - glActiveTexture(GL_TEXTURE0); - } - } else { - if (usedDepthBuffers >= 2) { - // copy depth buffer to depthtex1 - glActiveTexture(GL_TEXTURE11); - checkGLError("pre copy depth"); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, renderWidth, renderHeight); - checkGLError("copy depth"); - glActiveTexture(GL_TEXTURE0); - } - ShadersTex.bindNSTextures(defaultTexture.angelica$getMultiTexID()); // flat - } - } - - // public static void beginWaterFancy() { - // if (isRenderingWorld) - // { - // if (!isShadowPass) { - // // program water - // useProgram(Shaders.ProgramWater); - // GL11.glEnable(GL11.GL_BLEND); - // //setDrawBuffers(drawBuffersNone); - // } - // } - // } - // - // public static void midWaterFancy() { - // if (isRenderingWorld) - // { - // if (!isShadowPass) { - // setDrawBuffers(programsDrawBuffers[Shaders.ProgramWater]); - // } - // } - // } - - public static void beginWater() { - if (isRenderingWorld) { - if (!isShadowPass) { - // program water - useProgram(Shaders.ProgramWater); - GL11.glEnable(GL11.GL_BLEND); - GL11.glDepthMask(true); - } else { - GL11.glDepthMask(true); - } - } - } - - public static void endWater() { - if (isRenderingWorld) { - if (isShadowPass) { - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, - // sfbDepthTexture, 0); - } else { - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, - // dfbDepthTexture, 0); - } - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - } - } - - public static void readCenterDepth() { - if (!isShadowPass) { - // Read depth buffer at center of screen for DOF - if (centerDepthSmoothEnabled) { - tempDirectFloatBuffer.clear(); - glReadPixels( - renderWidth / 2, - renderHeight / 2, - 1, - 1, - GL11.GL_DEPTH_COMPONENT, - GL11.GL_FLOAT, - tempDirectFloatBuffer); - centerDepth = tempDirectFloatBuffer.get(0); - - // Smooth depth value - float fadeScalar = diffSystemTime * 0.01f; - float fadeFactor = (float) Math.exp(Math.log(0.5) * fadeScalar / centerDepthSmoothHalflife); - centerDepthSmooth = centerDepthSmooth * (fadeFactor) + centerDepth * (1 - fadeFactor); - } - } - } - - public static void beginWeather() { - if (!isShadowPass) { - if (usedDepthBuffers >= 3) { - // copy depth buffer to depthtex2 - glActiveTexture(GL_TEXTURE12); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, renderWidth, renderHeight); - glActiveTexture(GL_TEXTURE0); - } - // GL11.glDepthMask(false); - glEnable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_ALPHA_TEST); - useProgram(Shaders.ProgramWeather); - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, - // dfbWaterDepthTexture, 0); - } - - // if (isShadowPass) { - // glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); // will be set to sbf in endHand() - // } - } - - public static void endWeather() { - // GL11.glDepthMask(true); - glDisable(GL_BLEND); - useProgram(ProgramTexturedLit); - // glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, dfbDepthTexture, 0); - - // if (isShadowPass) { - // glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, sfb); // was set to 0 in beginWeather() - // } - } - - public static void beginProjectRedHalo() { - useProgram(ProgramBasic); - } - - public static void endProjectRedHalo() { - useProgram(ProgramTexturedLit); - } - - public static void applyHandDepth() { - if (Shaders.configHandDepthMul != 1.0) { - GL11.glScaled(1.0, 1.0, Shaders.configHandDepthMul); - } - } - - public static void beginHand() { - // glEnable(GL_BLEND); - // glDisable(GL_BLEND); - glMatrixMode(GL_MODELVIEW); - glPushMatrix(); - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - glMatrixMode(GL_MODELVIEW); - useProgram(Shaders.ProgramHand); - checkGLError("beginHand"); - checkFramebufferStatus("beginHand"); - } - - public static void endHand() { - // glDisable(GL_BLEND); - checkGLError("pre endHand"); - checkFramebufferStatus("pre endHand"); - glMatrixMode(GL_PROJECTION); - glPopMatrix(); - glMatrixMode(GL_MODELVIEW); - glPopMatrix(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - // useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - checkGLError("endHand"); - } - - public static void beginFPOverlay() { - // GL11.glDisable(GL11.GL_BLEND); - // GL11.glEnable(GL11.GL_BLEND); - // GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - public static void endFPOverlay() { - // GL11.glDisable(GL11.GL_BLEND); - } - - // ---------------------------------------- - - public static void glEnableWrapper(int cap) { - glEnable(cap); - if (cap == GL_TEXTURE_2D) enableTexture2D(); - else if (cap == GL_FOG) enableFog(); - } - - public static void glDisableWrapper(int cap) { - glDisable(cap); - if (cap == GL_TEXTURE_2D) disableTexture2D(); - else if (cap == GL_FOG) disableFog(); - } - - public static void sglEnableT2D(int cap) { - glEnable(cap); // GL_TEXTURE_2D - enableTexture2D(); - } - - public static void sglDisableT2D(int cap) { - glDisable(cap); // GL_TEXTURE_2D - disableTexture2D(); - } - - public static void sglEnableFog(int cap) { - glEnable(cap); // GL_FOG - enableFog(); - } - - public static void sglDisableFog(int cap) { - glDisable(cap); // GL_FOG - disableFog(); - } - - public static void enableTexture2D() { - if (isRenderingSky) { - useProgram(ProgramSkyTextured); - } else if (activeProgram == ProgramBasic) { - useProgram(lightmapEnabled ? ProgramTexturedLit : ProgramTextured); - } - } - - public static void disableTexture2D() { - if (isRenderingSky) { - useProgram(ProgramSkyBasic); - } else if (activeProgram == ProgramTextured || activeProgram == ProgramTexturedLit) { - useProgram(ProgramBasic); - } - } - - public static void enableFog() { - fogEnabled = true; - setProgramUniform1i("fogMode", fogMode); - } - - public static void disableFog() { - fogEnabled = false; - setProgramUniform1i("fogMode", 0); - } - - public static void sglFogi(int pname, int param) { - GL11.glFogi(pname, param); - if (pname == GL11.GL_FOG_MODE) { - fogMode = param; - if (fogEnabled) setProgramUniform1i("fogMode", fogMode); - } - } - - public static void enableLightmap() { - lightmapEnabled = true; - if (activeProgram == ProgramTextured) { - useProgram(ProgramTexturedLit); - } - } - - public static void disableLightmap() { - lightmapEnabled = false; - if (activeProgram == ProgramTexturedLit) { - useProgram(ProgramTextured); - } - } - - public static int entityData[] = new int[32]; - public static int entityDataIndex = 0; - - public static int getEntityData() { - return entityData[entityDataIndex * 2]; - } - - public static int getEntityData2() { - return entityData[entityDataIndex * 2 + 1]; - } - - public static int setEntityData1(int data1) { - entityData[entityDataIndex * 2] = (entityData[entityDataIndex * 2] & 0xFFFF) | (data1 << 16); - return data1; - } - - public static int setEntityData2(int data2) { - entityData[entityDataIndex * 2 + 1] = (entityData[entityDataIndex * 2 + 1] & 0xFFFF0000) | (data2 & 0xFFFF); - return data2; - } - - public static void pushEntity(int data0, int data1) { - entityDataIndex++; - entityData[entityDataIndex * 2] = (data0 & 0xFFFF) | (data1 << 16); - entityData[entityDataIndex * 2 + 1] = 0; - } - - public static void pushEntity(int data0) { - entityDataIndex++; - entityData[entityDataIndex * 2] = (data0 & 0xFFFF); - entityData[entityDataIndex * 2 + 1] = 0; - } - - public static void pushEntity(Block block) { - entityDataIndex++; - entityData[entityDataIndex * 2] = (Block.blockRegistry.getIDForObject(block) & 0xFFFF) - | (block.getRenderType() << 16); - entityData[entityDataIndex * 2 + 1] = 0; - } - - public static void pushEntity(RenderBlocks rb, Block block, int x, int y, int z) { - entityDataIndex++; - entityData[entityDataIndex * 2] = (Block.blockRegistry.getIDForObject(block) & 0xFFFF) - | (block.getRenderType() << 16); - entityData[entityDataIndex * 2 + 1] = rb.blockAccess.getBlockMetadata(x, y, z); - } - - public static void popEntity() { - entityData[entityDataIndex * 2] = 0; - entityData[entityDataIndex * 2 + 1] = 0; - entityDataIndex--; - } - - // ---------------------------------------- - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShadersRender.java b/src/main/java/com/gtnewhorizons/angelica/client/ShadersRender.java deleted file mode 100644 index e5c81a8b9..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShadersRender.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.nio.IntBuffer; - -import net.minecraft.block.Block; -import net.minecraft.block.BlockIce; -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.ItemRenderer; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.WorldRenderer; -import net.minecraft.client.renderer.culling.Frustrum; -import net.minecraft.item.Item; -import net.minecraft.item.ItemBlock; - -import org.lwjgl.opengl.GL11; - -public class ShadersRender { - - public static void setFrustrumPosition(Frustrum frustrum, double x, double y, double z) { - frustrum.setPosition(x, y, z); - } - - public static void clipRenderersByFrustrum(RenderGlobal renderGlobal, Frustrum frustrum, float par2) { - Shaders.checkGLError("pre clip"); - WorldRenderer[] worldRenderers = renderGlobal.worldRenderers; - if (!Shaders.isShadowPass) { - for (WorldRenderer worldRenderer : worldRenderers) { - if (!worldRenderer.skipAllRenderPasses()) { - worldRenderer.updateInFrustum(frustrum); - } - } - } else { - for (WorldRenderer worldRenderer : worldRenderers) { - if (!worldRenderer.skipAllRenderPasses()) { - worldRenderer.isInFrustum = true; - } - } - } - } - - public static void renderHand0(EntityRenderer er, float par1, int par2) { - if (!Shaders.isShadowPass) { - Item item = (Shaders.itemToRender != null) ? Shaders.itemToRender.getItem() : null; - Block block = (item instanceof ItemBlock) ? ((ItemBlock) item).field_150939_a : null; - // ItemCloth is for semitransparent block : stained glass, wool - if (!(item instanceof ItemBlock) && !(block instanceof BlockIce)) { - Shaders.readCenterDepth(); - Shaders.beginHand(); - er.renderHand(par1, par2); - Shaders.endHand(); - Shaders.isHandRendered = true; - } - } - } - - public static void renderHand1(EntityRenderer er, float par1, int par2) { - if (!Shaders.isShadowPass) { - if (!Shaders.isHandRendered) { - Shaders.readCenterDepth(); - GL11.glEnable(GL11.GL_BLEND); - Shaders.beginHand(); - er.renderHand(par1, par2); - Shaders.endHand(); - Shaders.isHandRendered = true; - } - } - } - - public static void renderItemFP(ItemRenderer itemRenderer, float par1) { - // clear depth buffer locally - GL11.glDepthFunc(GL11.GL_GEQUAL); - GL11.glPushMatrix(); - IntBuffer drawBuffers = Shaders.activeDrawBuffers; - Shaders.setDrawBuffers(Shaders.drawBuffersNone); - itemRenderer.renderItemInFirstPerson(par1); - Shaders.setDrawBuffers(drawBuffers); - GL11.glPopMatrix(); - GL11.glDepthFunc(GL11.GL_LEQUAL); - // really render item or hand - itemRenderer.renderItemInFirstPerson(par1); - } - - public static void renderFPOverlay(EntityRenderer er, float par1, int par2) { - Shaders.beginFPOverlay(); - er.renderHand(par1, par2); - Shaders.endFPOverlay(); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShadersTess.java b/src/main/java/com/gtnewhorizons/angelica/client/ShadersTess.java deleted file mode 100644 index 76c0d96b4..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShadersTess.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import static org.lwjgl.opengl.ARBVertexShader.glDisableVertexAttribArrayARB; -import static org.lwjgl.opengl.ARBVertexShader.glEnableVertexAttribArrayARB; -import static org.lwjgl.opengl.ARBVertexShader.glVertexAttribPointerARB; - -import java.nio.FloatBuffer; -import java.nio.ShortBuffer; -import java.util.Arrays; - -import net.minecraft.client.renderer.OpenGlHelper; -import net.minecraft.client.renderer.Tessellator; - -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL13; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; -import com.gtnewhorizons.angelica.mixins.interfaces.TessellatorAccessor; - -public class ShadersTess { - - public static final int vertexStride = 16; - - public static int draw(Tessellator tess) { - final TessellatorAccessor tessa = (TessellatorAccessor) tess; - if (!tess.isDrawing) { - throw new IllegalStateException("Not tesselating!"); - } else { - tess.isDrawing = false; - if (tess.drawMode == GL11.GL_QUADS && tess.vertexCount % 4 != 0) { - AngelicaTweaker.LOGGER.warn("%s", "bad vertexCount"); - } - int voffset = 0; - int realDrawMode = tess.drawMode; - while (voffset < tess.vertexCount) { - int vcount; - vcount = Math.min( - tess.vertexCount - voffset, - tessa.angelica$getByteBuffer().capacity() / (vertexStride * 4)); - if (realDrawMode == GL11.GL_QUADS) vcount = vcount / 4 * 4; - tessa.angelica$getFloatBuffer().clear(); - tessa.angelica$getShortBuffer().clear(); - tessa.angelica$getIntBuffer().clear(); - tessa.angelica$getIntBuffer().put(tess.rawBuffer, voffset * vertexStride, vcount * vertexStride); - tessa.angelica$getByteBuffer().position(0); - tessa.angelica$getByteBuffer().limit(vcount * (vertexStride * 4)); - voffset += vcount; - - if (tess.hasTexture) { - tessa.angelica$getFloatBuffer().position(3); - GL11.glTexCoordPointer(2, (vertexStride * 4), tessa.angelica$getFloatBuffer()); - GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - } - if (tess.hasBrightness) { - OpenGlHelper.setClientActiveTexture(OpenGlHelper.lightmapTexUnit); - tessa.angelica$getShortBuffer().position(6 * 2); - GL11.glTexCoordPointer(2, (vertexStride * 4), tessa.angelica$getShortBuffer()); - GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - OpenGlHelper.setClientActiveTexture(OpenGlHelper.defaultTexUnit); - } - if (tess.hasColor) { - tessa.angelica$getByteBuffer().position(5 * 4); - GL11.glColorPointer(4, true, (vertexStride * 4), tessa.angelica$getByteBuffer()); - GL11.glEnableClientState(GL11.GL_COLOR_ARRAY); - } - if (tess.hasNormals) { - tessa.angelica$getFloatBuffer().position(9); - GL11.glNormalPointer((vertexStride * 4), tessa.angelica$getFloatBuffer()); - GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY); - } - tessa.angelica$getFloatBuffer().position(0); - GL11.glVertexPointer(3, (vertexStride * 4), tessa.angelica$getFloatBuffer()); - ShadersTess.preDrawArray(tess); - - GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY); - GL11.glDrawArrays(realDrawMode, 0, vcount); - } - // end loop - - GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY); - ShadersTess.postDrawArray(tess); - if (tess.hasTexture) { - GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - } - if (tess.hasBrightness) { - OpenGlHelper.setClientActiveTexture(OpenGlHelper.lightmapTexUnit); - GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - OpenGlHelper.setClientActiveTexture(OpenGlHelper.defaultTexUnit); - } - if (tess.hasColor) { - GL11.glDisableClientState(GL11.GL_COLOR_ARRAY); - } - if (tess.hasNormals) { - GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY); - } - - int n = tess.rawBufferIndex * 4; - tess.reset(); - return n; - } - } - - public static void preDrawArray(Tessellator tess) { - final TessellatorAccessor tessa = (TessellatorAccessor) tess; - // Shaders.checkGLError("preDrawArray"); - if (Shaders.useMultiTexCoord3Attrib && tess.hasTexture) { - GL13.glClientActiveTexture(GL13.GL_TEXTURE3); - GL11.glTexCoordPointer(2, (vertexStride * 4), (FloatBuffer) tessa.angelica$getFloatBuffer().position(12)); - GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - GL13.glClientActiveTexture(GL13.GL_TEXTURE0); - } - if (Shaders.useMidTexCoordAttrib && tess.hasTexture) { - glVertexAttribPointerARB( - Shaders.midTexCoordAttrib, - 2, - false, - vertexStride * 4, - (FloatBuffer) tessa.angelica$getFloatBuffer().position(12)); - glEnableVertexAttribArrayARB(Shaders.midTexCoordAttrib); - } - if (Shaders.useEntityAttrib) { - glVertexAttribPointerARB( - Shaders.entityAttrib, - 3, - false, - false, - vertexStride * 4, - (ShortBuffer) tessa.angelica$getShortBuffer().position(7 * 2)); - glEnableVertexAttribArrayARB(Shaders.entityAttrib); - } - // Shaders.checkGLError("preDrawArray"); - } - - public static void preDrawArrayVBO(Tessellator tess) { - // Shaders.checkGLError("preDrawArray"); - if (Shaders.useMultiTexCoord3Attrib && tess.hasTexture) { - GL13.glClientActiveTexture(GL13.GL_TEXTURE3); - GL11.glTexCoordPointer(2, GL11.GL_FLOAT, (vertexStride * 4), 12 * 4L); - GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - GL13.glClientActiveTexture(GL13.GL_TEXTURE0); - } - if (Shaders.useMidTexCoordAttrib && tess.hasTexture) { - glVertexAttribPointerARB(Shaders.midTexCoordAttrib, 2, GL11.GL_FLOAT, false, vertexStride * 4, 12 * 4L); - glEnableVertexAttribArrayARB(Shaders.midTexCoordAttrib); - } - if (Shaders.useEntityAttrib) { - glVertexAttribPointerARB(Shaders.entityAttrib, 3, GL11.GL_SHORT, false, vertexStride * 4, 7 * 4L); - glEnableVertexAttribArrayARB(Shaders.entityAttrib); - } - // Shaders.checkGLError("preDrawArray"); - } - - public static void postDrawArray(Tessellator tess) { - // Shaders.checkGLError("postDrawArray"); - if (Shaders.useEntityAttrib) { - glDisableVertexAttribArrayARB(Shaders.entityAttrib); - } - if (Shaders.useMidTexCoordAttrib && tess.hasTexture) { - glDisableVertexAttribArrayARB(Shaders.midTexCoordAttrib); - } - if (Shaders.useMultiTexCoord3Attrib && tess.hasTexture) { - GL13.glClientActiveTexture(GL13.GL_TEXTURE3); - GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY); - GL13.glClientActiveTexture(GL13.GL_TEXTURE0); - } - // Shaders.checkGLError("postDrawArray"); - } - - public static void addVertex(Tessellator tess, double parx, double pary, double parz) { - final TessellatorAccessor tessa = (TessellatorAccessor) (Object) tess; - int[] rawBuffer = tess.rawBuffer; - int rbi = tess.rawBufferIndex; - float fx = (float) (parx + tess.xOffset); - float fy = (float) (pary + tess.yOffset); - float fz = (float) (parz + tess.zOffset); - // Check if buffer is nearly full - if (rbi >= tess.bufferSize - vertexStride * 4) { - if (tess.bufferSize >= 0x1000000) { - // Max size reached. Just draw. - if (tess.addedVertices % 4 == 0) { - tess.draw(); - tess.isDrawing = true; - } - } else if (tess.bufferSize > 0) { - // Expand - tess.bufferSize *= 2; - tess.rawBuffer = rawBuffer = Arrays.copyOf(tess.rawBuffer, tess.bufferSize); - AngelicaTweaker.LOGGER.trace("Expand tesselator buffer {}", tess.bufferSize); - } else { - // Initialize - tess.bufferSize = 0x10000; - tess.rawBuffer = rawBuffer = new int[tess.bufferSize]; - } - } - - // shaders mod calculate normal and mid UV - if (tess.drawMode == 7) { - int i = tess.addedVertices % 4; - float[] vertexPos = tessa.angelica$getVertexPos(); - vertexPos[i * 4 + 0] = fx; - vertexPos[i * 4 + 1] = fy; - vertexPos[i * 4 + 2] = fz; - if (i == 3) { - // calculate normal - float x1 = vertexPos[8 + 0] - vertexPos[+0]; - float y1 = vertexPos[8 + 1] - vertexPos[+1]; - float z1 = vertexPos[8 + 2] - vertexPos[+2]; - float x2 = vertexPos[12 + 0] - vertexPos[4 + 0]; - float y2 = vertexPos[12 + 1] - vertexPos[4 + 1]; - float z2 = vertexPos[12 + 2] - vertexPos[4 + 2]; - float vnx = y1 * z2 - y2 * z1; - float vny = z1 * x2 - z2 * x1; - float vnz = x1 * y2 - x2 * y1; - float lensq = vnx * vnx + vny * vny + vnz * vnz; - float mult = (lensq != 0.0) ? (float) (1f / Math.sqrt(lensq)) : 1f; - float normalX = vnx * mult; - float normalY = vny * mult; - float normalZ = vnz * mult; - tessa.angelica$setNormalX(normalX); - tessa.angelica$setNormalY(normalY); - tessa.angelica$setNormalZ(normalZ); - rawBuffer[rbi + (9 - vertexStride * 3)] = rawBuffer[rbi + (9 - vertexStride * 2)] = rawBuffer[rbi - + (9 - vertexStride * 1)] = Float.floatToRawIntBits(normalX); - rawBuffer[rbi + (10 - vertexStride * 3)] = rawBuffer[rbi + (10 - vertexStride * 2)] = rawBuffer[rbi - + (10 - vertexStride * 1)] = Float.floatToRawIntBits(normalY); - rawBuffer[rbi + (11 - vertexStride * 3)] = rawBuffer[rbi + (11 - vertexStride * 2)] = rawBuffer[rbi - + (11 - vertexStride * 1)] = Float.floatToRawIntBits(normalZ); - tess.hasNormals = true; - // mid UV - tessa.angelica$setMidTextureU( - (Float.intBitsToFloat(rawBuffer[rbi + (3 - vertexStride * 3)]) - + Float.intBitsToFloat(rawBuffer[rbi + (3 - vertexStride * 2)]) - + Float.intBitsToFloat(rawBuffer[rbi + (3 - vertexStride * 1)]) - + (float) tess.textureU) / 4); - tessa.angelica$setMidTextureV( - (Float.intBitsToFloat(rawBuffer[rbi + (4 - vertexStride * 3)]) - + Float.intBitsToFloat(rawBuffer[rbi + (4 - vertexStride * 2)]) - + Float.intBitsToFloat(rawBuffer[rbi + (4 - vertexStride * 1)]) - + (float) tess.textureV) / 4); - rawBuffer[rbi + (12 - vertexStride * 3)] = rawBuffer[rbi + (12 - vertexStride * 2)] = rawBuffer[rbi - + (12 - vertexStride * 1)] = Float.floatToRawIntBits(tessa.angelica$getMidTextureU()); - rawBuffer[rbi + (13 - vertexStride * 3)] = rawBuffer[rbi + (13 - vertexStride * 2)] = rawBuffer[rbi - + (13 - vertexStride * 1)] = Float.floatToRawIntBits(tessa.angelica$getMidTextureV()); - } - } - // end normal and mid UV calculation - - ++tess.addedVertices; - rawBuffer[rbi + 0] = Float.floatToRawIntBits((float) fx); - rawBuffer[rbi + 1] = Float.floatToRawIntBits((float) fy); - rawBuffer[rbi + 2] = Float.floatToRawIntBits((float) fz); - rawBuffer[rbi + 3] = Float.floatToRawIntBits((float) tess.textureU); - rawBuffer[rbi + 4] = Float.floatToRawIntBits((float) tess.textureV); - rawBuffer[rbi + 5] = tess.color; - rawBuffer[rbi + 6] = tess.brightness; - rawBuffer[rbi + 7] = Shaders.getEntityData(); - rawBuffer[rbi + 8] = Shaders.getEntityData2(); - rawBuffer[rbi + 9] = Float.floatToRawIntBits((float) tessa.angelica$getNormalX()); - rawBuffer[rbi + 10] = Float.floatToRawIntBits((float) tessa.angelica$getNormalY()); - rawBuffer[rbi + 11] = Float.floatToRawIntBits((float) tessa.angelica$getNormalZ()); - rawBuffer[rbi + 12] = Float.floatToRawIntBits((float) tessa.angelica$getMidTextureU()); - rawBuffer[rbi + 13] = Float.floatToRawIntBits((float) ((TessellatorAccessor) tess).angelica$getMidTextureV()); - - tess.rawBufferIndex = rbi += vertexStride; - ++tess.vertexCount; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/ShadersTex.java b/src/main/java/com/gtnewhorizons/angelica/client/ShadersTex.java deleted file mode 100644 index f2d0aa357..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/client/ShadersTex.java +++ /dev/null @@ -1,1005 +0,0 @@ -package com.gtnewhorizons.angelica.client; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.IntBuffer; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import javax.imageio.ImageIO; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.AbstractTexture; -import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.client.renderer.texture.ITextureObject; -import net.minecraft.client.renderer.texture.LayeredTexture; -import net.minecraft.client.renderer.texture.Stitcher; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.client.renderer.texture.TextureMap; -import net.minecraft.client.renderer.texture.TextureUtil; -import net.minecraft.client.resources.IResource; -import net.minecraft.client.resources.IResourceManager; -import net.minecraft.util.ResourceLocation; - -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.lwjgl.opengl.GL13; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -public class ShadersTex { - - public static final int initialBufferSize = 1048576; - public static ByteBuffer byteBuffer = BufferUtils.createByteBuffer(initialBufferSize * 4); - public static IntBuffer intBuffer = byteBuffer.asIntBuffer(); - public static int[] intArray = new int[initialBufferSize]; - public static final int defBaseTexColor = 0x00000000; - public static final int defNormTexColor = 0xFF7F7FFF; - public static final int defSpecTexColor = 0x00000000; - public static Map multiTexMap = new HashMap<>(); - public static TextureMap updatingTextureMap = null; - public static TextureAtlasSprite updatingSprite = null; - public static MultiTexID updatingTex = null; - public static MultiTexID boundTex = null; - public static int updatingPage = 0; - public static String iconName = null; - - public static IntBuffer getIntBuffer(int size) { - if (intBuffer.capacity() < size) { - int bufferSize = roundUpPOT(size); - byteBuffer = BufferUtils.createByteBuffer(bufferSize * 4); - intBuffer = byteBuffer.asIntBuffer(); - } - return intBuffer; - } - - public static int[] getIntArray(int size) { - if (intArray.length < size) { - intArray = null; - intArray = new int[roundUpPOT(size)]; - } - return intArray; - } - - public static int roundUpPOT(int x) { - int i = x - 1; - i |= (i >> 1); - i |= (i >> 2); - i |= (i >> 4); - i |= (i >> 8); - i |= (i >> 16); - return i + 1; - } - - public static IntBuffer fillIntBuffer(int size, int value) { - getIntArray(size); - getIntBuffer(size); - Arrays.fill(intArray, 0, size, value); - intBuffer.put(intArray, 0, size); - return intBuffer; - } - - public static int[] createAIntImage(int size) { - int[] aint = new int[size * 3]; - Arrays.fill(aint, 0, size, defBaseTexColor); - Arrays.fill(aint, size, size * 2, defNormTexColor); - Arrays.fill(aint, size * 2, size * 3, defSpecTexColor); - return aint; - } - - public static int[] createAIntImage(int size, int color) { - int[] aint = new int[size * 3]; - Arrays.fill(aint, 0, size, color); - Arrays.fill(aint, size, size * 2, defNormTexColor); - Arrays.fill(aint, size * 2, size * 3, defSpecTexColor); - return aint; - } - - public static MultiTexID getMultiTexID(AbstractTexture tex) { - MultiTexID multiTex = tex.angelica$multiTex; - if (multiTex == null) { - int baseTex = tex.getGlTextureId(); - multiTex = multiTexMap.get(Integer.valueOf(baseTex)); - if (multiTex == null) { - multiTex = new MultiTexID(baseTex, GL11.glGenTextures(), GL11.glGenTextures()); - multiTexMap.put(baseTex, multiTex); - } - tex.angelica$multiTex = multiTex; - } - return multiTex; - } - - public static void deleteTextures(AbstractTexture atex) { - int texid = atex.glTextureId; - if (texid != -1) { - GL11.glDeleteTextures(texid); - atex.glTextureId = -1; - } - MultiTexID multiTex = atex.angelica$multiTex; - if (multiTex != null) { - atex.angelica$multiTex = null; - multiTexMap.remove(Integer.valueOf(multiTex.base)); - GL11.glDeleteTextures(multiTex.norm); - GL11.glDeleteTextures(multiTex.spec); - if (multiTex.base != texid) { - AngelicaTweaker.LOGGER.warn("Error : MultiTexID.base mismatch."); - GL11.glDeleteTextures(multiTex.base); - } - } - } - - /** Remove MultiTexID object reference and delete textures */ - public static int deleteMultiTex(ITextureObject tex) { - if (tex instanceof AbstractTexture) { - deleteTextures((AbstractTexture) tex); - } else { - GL11.glDeleteTextures(tex.getGlTextureId()); - } - return 0; - } - - public static void bindNSTextures(int normTex, int specTex) { - // Shaders.checkGLError("pre bindNSTextures"); - if (Shaders.isRenderingWorld && Shaders.activeTexUnit == GL13.GL_TEXTURE0) { - GL13.glActiveTexture(GL13.GL_TEXTURE2); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, normTex); - GL13.glActiveTexture(GL13.GL_TEXTURE3); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, specTex); - GL13.glActiveTexture(GL13.GL_TEXTURE0); - } - // Shaders.checkGLError("bindNSTextures"); - } - - public static void bindNSTextures(MultiTexID multiTex) { - bindNSTextures(multiTex.norm, multiTex.spec); - } - - public static void bindTextures(int baseTex, int normTex, int specTex) { - // Shaders.checkGLError("pre bindTextures"); - if (Shaders.isRenderingWorld && Shaders.activeTexUnit == GL13.GL_TEXTURE0) { - GL13.glActiveTexture(GL13.GL_TEXTURE2); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, normTex); - GL13.glActiveTexture(GL13.GL_TEXTURE3); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, specTex); - GL13.glActiveTexture(GL13.GL_TEXTURE0); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, baseTex); - // Shaders.checkGLError("bindTextures"); - } - - public static void bindTextures(MultiTexID multiTex) { - boundTex = multiTex; - if (Shaders.isRenderingWorld && Shaders.activeTexUnit == GL13.GL_TEXTURE0) { - if (Shaders.configNormalMap) { - GL13.glActiveTexture(GL13.GL_TEXTURE2); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - } - if (Shaders.configSpecularMap) { - GL13.glActiveTexture(GL13.GL_TEXTURE3); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - } - GL13.glActiveTexture(GL13.GL_TEXTURE0); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - } - - public static void bindTexture(ITextureObject tex) { - if (tex instanceof TextureMap) { - Shaders.atlasSizeX = ((TextureMap) tex).angelica$atlasWidth; - Shaders.atlasSizeY = ((TextureMap) tex).angelica$atlasHeight; - } else { - Shaders.atlasSizeX = 0; - Shaders.atlasSizeY = 0; - } - bindTextures(tex.angelica$getMultiTexID()); - } - - /** not used */ - public static void bindTextures(int baseTex) { - MultiTexID multiTex = multiTexMap.get(Integer.valueOf(baseTex)); - bindTextures(multiTex); - } - - public static void allocTexStorage(int width, int height, int maxLevel) { - Shaders.checkGLError("pre allocTexStorage"); - int level; - for (level = 0; (width >> level) > 0 && (height >> level) > 0 /* && level<=maxLevel */; ++level) { - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - level, - GL11.GL_RGBA, - (width >> level), - (height >> level), - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - (IntBuffer) null); - } - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, level - 1); - Shaders.checkGLError("allocTexStorage"); - // clear unused level otherwise glTexSubImage2D will crash on AMD when reallocation texture of different size. - // for ( ; level < 16 ; ++level) { - // GL11.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA, 0, 0, 0, GL12.GL_BGRA, - // GL12.GL_UNSIGNED_INT_8_8_8_8_REV, (IntBuffer)null); - // } - GL11.glGetError(); // It usually returns error 0x0501 Invalid value for width 0 height 0. Ignore it. - } - - // for Dynamic Texture - public static void initDynamicTexture(int texID, int width, int height, DynamicTexture tex) { - MultiTexID multiTex = tex.angelica$getMultiTexID(); - int[] aint = tex.getTextureData(); - int size = width * height; - Arrays.fill(aint, size, size * 2, defNormTexColor); - Arrays.fill(aint, size * 2, size * 3, defSpecTexColor); - // base texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - allocTexStorage(width, height, 0); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0); - - // norm texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - allocTexStorage(width, height, 0); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0); - - // spec texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - allocTexStorage(width, height, 0); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0); - - // base texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - } - - public static ITextureObject createDefaultTexture() { - DynamicTexture tex = new DynamicTexture(1, 1); - tex.getTextureData()[0] = 0xffffffff; - tex.updateDynamicTexture(); - return tex; - } - - // for TextureMap - public static void allocateTextureMap(int texID, int mipmapLevels, int width, int height, float anisotropy, - Stitcher stitcher, TextureMap tex) { - AngelicaTweaker.LOGGER.trace( - "allocateTextureMap {} {} {} {} {}", - tex.getTextureType(), - mipmapLevels, - width, - height, - anisotropy); - updatingTextureMap = tex; - tex.angelica$atlasWidth = width; - tex.angelica$atlasHeight = height; - MultiTexID multiTex = getMultiTexID(tex); - updatingTex = multiTex; - TextureUtil.allocateTextureImpl(multiTex.base, mipmapLevels, width, height, anisotropy); - if (Shaders.configNormalMap) - TextureUtil.allocateTextureImpl(multiTex.norm, mipmapLevels, width, height, anisotropy); - if (Shaders.configSpecularMap) - TextureUtil.allocateTextureImpl(multiTex.spec, mipmapLevels, width, height, anisotropy); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texID); - } - - public static TextureAtlasSprite setSprite(TextureAtlasSprite tas) { - return updatingSprite = tas; - } - - public static String setIconName(String name) { - return iconName = name; - } - - public static void uploadTexSubForLoadAtlas(int[][] data, int width, int height, int xoffset, int yoffset, - boolean linear, boolean clamp) { - TextureUtil.uploadTextureMipmap(data, width, height, xoffset, yoffset, linear, clamp); - boolean border = updatingSprite.useAnisotropicFiltering; - int[][] aaint; - // - if (Shaders.configNormalMap) { - aaint = readImageAndMipmaps(iconName + "_n", width, height, data.length, border, defNormTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.norm); - TextureUtil.uploadTextureMipmap(aaint, width, height, xoffset, yoffset, linear, clamp); - } - // - if (Shaders.configSpecularMap) { - aaint = readImageAndMipmaps(iconName + "_s", width, height, data.length, border, defSpecTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.spec); - TextureUtil.uploadTextureMipmap(aaint, width, height, xoffset, yoffset, linear, clamp); - } - // - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.base); - } - - public static int[][] readImageAndMipmaps(String name, int width, int height, int numLevels, boolean border, - int defColor) { - int[][] aaint = new int[numLevels][]; - int[] aint; - aaint[0] = aint = new int[width * height]; - boolean goodImage = false; - BufferedImage image = readImage(updatingTextureMap.completeResourceLocation(new ResourceLocation(name), 0)); - if (image != null) { - int imageWidth = image.getWidth(); - int imageHeight = image.getHeight(); - if (imageWidth + (border ? 16 : 0) == width) { - goodImage = true; - image.getRGB(0, 0, imageWidth, imageWidth, aint, 0, imageWidth); - if (border) TextureUtil.prepareAnisotropicData(aint, imageWidth, imageWidth, 8); - } - } - if (!goodImage) { - Arrays.fill(aint, defColor); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.spec); - aaint = genMipmapsSimple(aaint.length - 1, width, aaint); - return aaint; - } - - public static BufferedImage readImage(ResourceLocation resLoc) { - BufferedImage image = null; - InputStream istr = null; - if (resManager != null) { - try { - istr = resManager.getResource(resLoc).getInputStream(); - image = ImageIO.read(istr); - } catch (IOException e) {} - if (istr != null) { - try { - istr.close(); - } catch (IOException e) {} - istr = null; - } - } - return image; - } - - public static int[][] genMipmapsSimple(int maxLevel, int width, int[][] data) { - int level; - for (level = 1; level <= maxLevel; ++level) { - if (data[level] == null) { - int cw = width >> level; - int pw = cw * 2; - int[] aintp = data[level - 1]; - int[] aintc = data[level] = new int[cw * cw]; - int x, y; - for (y = 0; y < cw; ++y) { - for (x = 0; x < cw; ++x) { - int ppos = y * 2 * pw + x * 2; - aintc[y * cw + x] = blend4Simple( - aintp[ppos], - aintp[ppos + 1], - aintp[ppos + pw], - aintp[ppos + pw + 1]); - } - } - } - } - return data; - } - - public static void uploadTexSub(int[][] data, int width, int height, int xoffset, int yoffset, boolean linear, - boolean clamp) { - TextureUtil.uploadTextureMipmap(data, width, height, xoffset, yoffset, linear, clamp); - } - - public static int blend4Alpha(int c0, int c1, int c2, int c3) { - int a0 = (c0 >>> 24) & 255; - int a1 = (c1 >>> 24) & 255; - int a2 = (c2 >>> 24) & 255; - int a3 = (c3 >>> 24) & 255; - int as = a0 + a1 + a2 + a3; - int an = (as + 2) / 4; - // int an = Math.min(Math.min(Math.min(a0,a1),a2),a3); - int dv; - if (as != 0) { - dv = as; - } else { - dv = 4; - a3 = a2 = a1 = a0 = 1; - } - int frac = (dv + 1) / 2; - // return (((Math.min(Math.min(Math.min(a0,a1),a2),a3)/ 4) << 24) | - int color = (an << 24) - | (((((c0 >>> 16) & 255) * a0 + ((c1 >>> 16) & 255) * a1 - + ((c2 >>> 16) & 255) * a2 - + ((c3 >>> 16) & 255) * a3 - + frac) / dv) << 16) - | (((((c0 >>> 8) & 255) * a0 + ((c1 >>> 8) & 255) * a1 - + ((c2 >>> 8) & 255) * a2 - + ((c3 >>> 8) & 255) * a3 - + frac) / dv) << 8) - | (((((c0 >>> 0) & 255) * a0 + ((c1 >>> 0) & 255) * a1 - + ((c2 >>> 0) & 255) * a2 - + ((c3 >>> 0) & 255) * a3 - + frac) / dv) << 0); - return color; - } - - public static int blend4Simple(int c0, int c1, int c2, int c3) { - int color = (((((c0 >>> 24) & 255) + ((c1 >>> 24) & 255) + ((c2 >>> 24) & 255) + ((c3 >>> 24) & 255) + 2) / 4) - << 24) - | (((((c0 >>> 16) & 255) + ((c1 >>> 16) & 255) + ((c2 >>> 16) & 255) + ((c3 >>> 16) & 255) + 2) / 4) - << 16) - | (((((c0 >>> 8) & 255) + ((c1 >>> 8) & 255) + ((c2 >>> 8) & 255) + ((c3 >>> 8) & 255) + 2) / 4) << 8) - | (((((c0 >>> 0) & 255) + ((c1 >>> 0) & 255) + ((c2 >>> 0) & 255) + ((c3 >>> 0) & 255) + 2) / 4) << 0); - return color; - } - - public static void genMipmapAlpha(int[] aint, int offset, int width, int height) { - int level; - int w1, w2, h1, h2, o1, o2; - w1 = w2 = width; - h1 = h2 = height; - o1 = o2 = offset; - // generate mipmap from big to small - o2 = offset; - w2 = width; - h2 = height; - o1 = 0; - w1 = 0; - h1 = 0; - for (level = 0; w2 > 1 && h2 > 1; ++level, w2 = w1, h2 = h1, o2 = o1) { - o1 = o2 + w2 * h2; - w1 = w2 / 2; - h1 = h2 / 2; - for (int y = 0; y < h1; ++y) { - int p1 = o1 + y * w1; - int p2 = o2 + y * 2 * w2; - for (int x = 0; x < w1; ++x) { - aint[p1 + x] = blend4Alpha( - aint[p2 + (x * 2)], - aint[p2 + (x * 2 + 1)], - aint[p2 + w2 + (x * 2)], - aint[p2 + w2 + (x * 2 + 1)]); - } - } - } - // fix black pixels from small to big - while (level > 0) { - --level; - w2 = width >> level; - h2 = height >> level; - o2 = o1 - w2 * h2; - int p2 = o2; - for (int y = 0; y < h2; ++y) { - for (int x = 0; x < w2; ++x) { - // p2 = o2 + y*w2 + x; - if (aint[p2] == 0) { - aint[p2] = aint[o1 + (y / 2) * w1 + (x / 2)] & 0x00ffffff; - } - ++p2; - } - } - o1 = o2; - w1 = w2; - h1 = h2; - } - } - - public static void genMipmapSimple(int[] aint, int offset, int width, int height) { - int level; - int w1, w2, h1, h2, o1, o2; - w1 = w2 = width; - h1 = h2 = height; - o1 = o2 = offset; - // generate mipmap from big to small - o2 = offset; - w2 = width; - h2 = height; - o1 = 0; - w1 = 0; - h1 = 0; - for (level = 0; w2 > 1 && h2 > 1; ++level, w2 = w1, h2 = h1, o2 = o1) { - o1 = o2 + w2 * h2; - w1 = w2 / 2; - h1 = h2 / 2; - for (int y = 0; y < h1; ++y) { - int p1 = o1 + y * w1; - int p2 = o2 + y * 2 * w2; - for (int x = 0; x < w1; ++x) { - aint[p1 + x] = blend4Simple( - aint[p2 + (x * 2)], - aint[p2 + (x * 2 + 1)], - aint[p2 + w2 + (x * 2)], - aint[p2 + w2 + (x * 2 + 1)]); - } - } - } - // fix black pixels from small to big - while (level > 0) { - --level; - w2 = width >> level; - h2 = height >> level; - o2 = o1 - w2 * h2; - int p2 = o2; - for (int y = 0; y < h2; ++y) { - for (int x = 0; x < w2; ++x) { - // p2 = o2 + y*w2 + x; - if (aint[p2] == 0) { - aint[p2] = aint[o1 + (y / 2) * w1 + (x / 2)] & 0x00ffffff; - } - ++p2; - } - } - o1 = o2; - w1 = w2; - h1 = h2; - } - } - - public static boolean isSemiTransparent(int[] aint, int width, int height) { - int size = width * height; - // grass side texture ?; - if (aint[0] >>> 24 == 255 && aint[size - 1] == 0) return true; - for (int i = 0; i < size; ++i) { - int alpha = aint[i] >>> 24; - if (alpha != 0 && alpha != 255) return true; - } - return false; - } - - public static void updateSubImage1(int[] src, int width, int height, int posX, int posY, int page, int color) { - int size = width * height; - IntBuffer intBuf = getIntBuffer(size); - int[] aint = getIntArray((size * 4 + 2) / 3); - if (src.length >= size * (page + 1)) { - System.arraycopy(src, size * page, aint, 0, size); - } else { - Arrays.fill(aint, color); - } - // - // if (page == 0) - genMipmapAlpha(aint, 0, width, height); - // else - // genMipmapSimple(aint,0,width,height); - // - for (int level = 0, offset = 0, lw = width, lh = height, px = posX, py = posY; lw > 0 && lh > 0; ++level) { - int lsize = lw * lh; - intBuf.clear(); - intBuf.put(aint, offset, lsize).position(0).limit(lsize); - GL11.glTexSubImage2D( - GL11.GL_TEXTURE_2D, - level, - px, - py, - lw, - lh, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - offset += lsize; - lw /= 2; - lh /= 2; - px /= 2; - py /= 2; - } - intBuf.clear(); - } - - public static void updateSubTex1(int[] src, int width, int height, int posX, int posY) { - int level; - int cw; - int ch; - int cx; - int cy; - for (level = 0, cw = width, ch = height, cx = posX, cy = posY; cw > 0 - && ch > 0; ++level, cw /= 2, ch /= 2, cx /= 2, cy /= 2) { - GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, level, cx, cy, 0, 0, cw, ch); - } - } - - /* updateSubImage for single Texture with mipmap */ - public static void updateSubImage1(int[][] src, int width, int height, int posX, int posY, int page, int color) { - int size = width * height; - IntBuffer intBuf = getIntBuffer(size); - int numLevel = src.length; - for (int level = 0, // offset=0, - lw = width, lh = height, px = posX, py = posY; lw > 0 && lh > 0 && level < numLevel; ++level) { - int lsize = lw * lh; - intBuf.clear(); - intBuf.put(src[level], 0 /* offset */, lsize).position(0).limit(lsize); - GL11.glTexSubImage2D( - GL11.GL_TEXTURE_2D, - level, - px, - py, - lw, - lh, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - // offset += lsize; - lw /= 2; - lh /= 2; - px /= 2; - py /= 2; - } - intBuf.clear(); - } - - public static void setupTextureMipmap(TextureMap tex) { - /* - * MultiTexID multiTex = tex.getMultiTexID(); GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - * GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - * GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - * GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); - */ - } - - public static void updateDynamicTexture(int texID, int[] src, int width, int height, DynamicTexture tex) { - MultiTexID multiTex = tex.angelica$getMultiTexID(); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - updateSubImage1(src, width, height, 0, 0, 1, defNormTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - updateSubImage1(src, width, height, 0, 0, 2, defSpecTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - updateSubImage1(src, width, height, 0, 0, 0, defBaseTexColor); - } - - public static void updateSubImage(int[] src, int width, int height, int posX, int posY, boolean linear, - boolean clamp) { - if (updatingTex != null) { - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.norm); - updateSubImage1(src, width, height, posX, posY, 1, defNormTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.spec); - updateSubImage1(src, width, height, posX, posY, 2, defSpecTexColor); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, updatingTex.base); - } - updateSubImage1(src, width, height, posX, posY, 0, defBaseTexColor); - } - - // not used - public static void updateAnimationTextureMap(TextureMap tex, List tasList) { - Iterator iterator; - MultiTexID multiTex = tex.angelica$getMultiTexID(); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - for (iterator = tasList.iterator(); iterator.hasNext();) { - TextureAtlasSprite tas = iterator.next(); - tas.updateAnimation(); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - for (iterator = tasList.iterator(); iterator.hasNext();) { - TextureAtlasSprite tas = iterator.next(); - tas.updateAnimation(); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - for (iterator = tasList.iterator(); iterator.hasNext();) { - TextureAtlasSprite tas = iterator.next(); - tas.updateAnimation(); - } - } - - public static void setupTexture(MultiTexID multiTex, int[] src, int width, int height, boolean linear, - boolean clamp) { - int mmfilter = linear ? GL11.GL_LINEAR : GL11.GL_NEAREST; - int wraptype = clamp ? GL11.GL_CLAMP : GL11.GL_REPEAT; - int size = width * height; - IntBuffer intBuf = getIntBuffer(size); - // - intBuf.clear(); - intBuf.put(src, 0, size).position(0).limit(size); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - 0, - GL11.GL_RGBA, - width, - height, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wraptype); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wraptype); - // - intBuf.put(src, size, size).position(0).limit(size); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - 0, - GL11.GL_RGBA, - width, - height, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wraptype); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wraptype); - // - intBuf.put(src, size * 2, size).position(0).limit(size); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - 0, - GL11.GL_RGBA, - width, - height, - 0, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, mmfilter); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wraptype); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wraptype); - // - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - } - - /* currently not used */ - public static void updateSubImage(MultiTexID multiTex, int[] src, int width, int height, int posX, int posY, - boolean linear, boolean clamp) { - int size = width * height; - IntBuffer intBuf = getIntBuffer(size); - // - intBuf.clear(); - intBuf.put(src, 0, size); - intBuf.position(0).limit(size); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexSubImage2D( - GL11.GL_TEXTURE_2D, - 0, - posX, - posY, - width, - height, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - if (src.length == size * 3) { - intBuf.clear(); - intBuf.put(src, size, size).position(0); - intBuf.position(0).limit(size); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexSubImage2D( - GL11.GL_TEXTURE_2D, - 0, - posX, - posY, - width, - height, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - if (src.length == size * 3) { - intBuf.clear(); - intBuf.put(src, size * 2, size); - intBuf.position(0).limit(size); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); - GL11.glTexSubImage2D( - GL11.GL_TEXTURE_2D, - 0, - posX, - posY, - width, - height, - GL12.GL_BGRA, - GL12.GL_UNSIGNED_INT_8_8_8_8_REV, - intBuf); - GL13.glActiveTexture(GL13.GL_TEXTURE0); - } - - public static ResourceLocation getNSMapLocation(ResourceLocation location, String mapName) { - String basename = location.getResourcePath(); - String[] basenameParts = basename.split(".png"); - String basenameNoFileType = basenameParts[0]; - return new ResourceLocation(location.getResourceDomain(), basenameNoFileType + "_" + mapName + ".png"); - } - - public static void loadNSMap(IResourceManager manager, ResourceLocation location, int width, int height, - int[] aint) { - if (Shaders.configNormalMap) ShadersTex.loadNSMap1( - manager, - getNSMapLocation(location, "n"), - width, - height, - aint, - width * height, - defNormTexColor); - if (Shaders.configSpecularMap) ShadersTex.loadNSMap1( - manager, - getNSMapLocation(location, "s"), - width, - height, - aint, - width * height * 2, - defSpecTexColor); - } - - public static void loadNSMap1(IResourceManager manager, ResourceLocation location, int width, int height, - int[] aint, int offset, int defaultColor) { - boolean good = false; - try { - IResource res = manager.getResource(location); - BufferedImage bufferedimage = ImageIO.read(res.getInputStream()); - if (bufferedimage.getWidth() == width && bufferedimage.getHeight() == height) { - bufferedimage.getRGB(0, 0, width, height, aint, offset, width); - good = true; - } - } catch (IOException ex) {} - if (!good) { - java.util.Arrays.fill(aint, offset, offset + width * height, defaultColor); - } - } - - /** init and upload from BufferedImage */ - /* - * Replacement for TextureUtil.func_110989_a call in SimpleTexture.func_110551_a. Keep par0...par5 the same as - * func_110989_a for easy patching. More parameters added for reading N-S-Map. - */ - public static int loadSimpleTexture(int textureID, BufferedImage bufferedimage, boolean linear, boolean clamp, - IResourceManager resourceManager, ResourceLocation location, MultiTexID multiTex) { - int width = bufferedimage.getWidth(); - int height = bufferedimage.getHeight(); - int size = width * height; - int[] aint = getIntArray(size * 3); - bufferedimage.getRGB(0, 0, width, height, aint, 0, width); - loadNSMap(resourceManager, location, width, height, aint); - setupTexture(multiTex, aint, width, height, linear, clamp); - return textureID; - } - - public static void mergeImage(int[] aint, int dstoff, int srcoff, int size) {} - - public static int blendColor(int color1, int color2, int factor1) { - int factor2 = 255 - factor1; - return (((((color1 >>> 24) & 255) * factor1 + ((color2 >>> 24) & 255) * factor2) / 255) << 24) - | (((((color1 >>> 16) & 255) * factor1 + ((color2 >>> 16) & 255) * factor2) / 255) << 16) - | (((((color1 >>> 8) & 255) * factor1 + ((color2 >>> 8) & 255) * factor2) / 255) << 8) - | (((((color1 >>> 0) & 255) * factor1 + ((color2 >>> 0) & 255) * factor2) / 255) << 0); - } - - public static void loadLayeredTexture(LayeredTexture tex, IResourceManager manager, List list) { - int width = 0; - int height = 0; - int size = 0; - int[] image = null; - Iterator iterator; - for (iterator = list.iterator(); iterator.hasNext();) { - String s = iterator.next(); - if (s != null) { - try { - ResourceLocation location = new ResourceLocation(s); - InputStream inputstream = manager.getResource(location).getInputStream(); - BufferedImage bufimg = ImageIO.read(inputstream); - - if (size == 0) { - width = bufimg.getWidth(); - height = bufimg.getHeight(); - size = width * height; - image = createAIntImage(size, 0x00000000); - } - int[] aint = getIntArray(size * 3); - bufimg.getRGB(0, 0, width, height, aint, 0, width); - loadNSMap(manager, location, width, height, aint); - // merge - for (int i = 0; i < size; ++i) { - int alpha = (aint[i] >>> 24) & 255; - image[size * 0 + i] = blendColor(aint[size * 0 + i], image[size * 0 + i], alpha); - image[size * 1 + i] = blendColor(aint[size * 1 + i], image[size * 1 + i], alpha); - image[size * 2 + i] = blendColor(aint[size * 2 + i], image[size * 2 + i], alpha); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - } - // init and upload - setupTexture(tex.angelica$getMultiTexID(), image, width, height, false, false); - } - - /* update block texture filter +/- items texture */ - static void updateTextureMinMagFilter() { - TextureManager texman = Minecraft.getMinecraft().getTextureManager(); - ITextureObject texObj = texman.getTexture(TextureMap.locationBlocksTexture); - if (texObj != null) { - MultiTexID multiTex = texObj.angelica$getMultiTexID(); - // base texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.base); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MIN_FILTER, - Shaders.texMinFilValue[Shaders.configTexMinFilB]); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MAG_FILTER, - Shaders.texMagFilValue[Shaders.configTexMagFilB]); - // norm texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.norm); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MIN_FILTER, - Shaders.texMinFilValue[Shaders.configTexMinFilN]); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MAG_FILTER, - Shaders.texMagFilValue[Shaders.configTexMagFilN]); - - // spec texture - GL11.glBindTexture(GL11.GL_TEXTURE_2D, multiTex.spec); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MIN_FILTER, - Shaders.texMinFilValue[Shaders.configTexMinFilS]); - GL11.glTexParameteri( - GL11.GL_TEXTURE_2D, - GL11.GL_TEXTURE_MAG_FILTER, - Shaders.texMagFilValue[Shaders.configTexMagFilS]); - - GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - } - } - - static IResourceManager resManager = null; - static ResourceLocation resLocation = null; - static int imageSize = 0; - - public static IResource loadResource(IResourceManager manager, ResourceLocation location) throws IOException { - resManager = manager; - resLocation = location; - return manager.getResource(location); - } - - public static int[] loadAtlasSprite(BufferedImage bufferedimage, int startX, int startY, int w, int h, int[] aint, - int offset, int scansize) { - imageSize = w * h; - bufferedimage.getRGB(startX, startY, w, h, aint, offset, scansize); - loadNSMap(resManager, resLocation, w, h, aint); - return aint; - } - - public static int[] extractFrame(int[] src, int width, int height, int frameIndex) { - int srcSize = imageSize; - int frameSize = width * height; - int[] dst = new int[frameSize * 3]; - int srcPos = frameSize * frameIndex; - int dstPos = 0; - System.arraycopy(src, srcPos, dst, dstPos, frameSize); - srcPos += srcSize; - dstPos += frameSize; - System.arraycopy(src, srcPos, dst, dstPos, frameSize); - srcPos += srcSize; - dstPos += frameSize; - System.arraycopy(src, srcPos, dst, dstPos, frameSize); - return dst; - } - - public static void fixTransparentColor(TextureAtlasSprite tas, int[] aint) {} -} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java new file mode 100644 index 000000000..1dead8301 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java @@ -0,0 +1,577 @@ +package com.gtnewhorizons.angelica.client.font; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor; +import it.unimi.dsi.fastutil.chars.Char2ShortOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import jss.util.RandomXoshiro256StarStar; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Objects; + +import static org.lwjgl.opengl.GL11.*; + +/** + * A batching replacement for {@code FontRenderer} + * + * @author eigenraven + */ +public class BatchingFontRenderer { + + /** The underlying FontRenderer object that's being accelerated */ + protected FontRenderer underlying; + /** Cached locations for each unicode page atlas */ + private final ResourceLocation[] unicodePageLocations; + /** Array of width of all the characters in default.png */ + protected int[] charWidth = new int[256]; + /** Array of the start/end column (in upper/lower nibble) for every glyph in the /font directory. */ + protected byte[] glyphWidth; + /** + * Array of RGB triplets defining the 16 standard chat colors followed by 16 darker version of the same colors for + * drop shadows. + */ + private int[] colorCode; + /** Location of the primary font atlas to bind. */ + protected final ResourceLocation locationFontTexture; + /** The RenderEngine used to load and setup glyph textures. */ + private final TextureManager renderEngine; + private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar(); + + /** The full list of characters present in the default Minecraft font, excluding the Unicode font */ + @SuppressWarnings("UnnecessaryUnicodeEscape") + private static final String MCFONT_CHARS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; + + private static final short[] MCFONT_ASCII_LUT = new short[512]; + private static final Char2ShortOpenHashMap MCFONT_UNI_LUT = new Char2ShortOpenHashMap(); + + static { + Arrays.fill(MCFONT_ASCII_LUT, (short) -1); + for (short i = 0; i < MCFONT_CHARS.length(); i++) { + char ch = MCFONT_CHARS.charAt(i); + if (ch < MCFONT_ASCII_LUT.length) { + MCFONT_ASCII_LUT[ch] = i; + } else { + MCFONT_UNI_LUT.put(ch, i); + } + } + } + + public static int lookupMcFontPosition(char ch) { + if (ch < MCFONT_ASCII_LUT.length) { + return MCFONT_ASCII_LUT[ch]; + } else { + return MCFONT_UNI_LUT.getOrDefault(ch, (short) -1); + } + } + + public BatchingFontRenderer(FontRenderer underlying, ResourceLocation[] unicodePageLocations, int[] charWidth, + byte[] glyphWidth, int[] colorCode, ResourceLocation locationFontTexture, TextureManager renderEngine) { + this.underlying = underlying; + this.unicodePageLocations = unicodePageLocations; + this.charWidth = charWidth; + this.glyphWidth = glyphWidth; + this.colorCode = colorCode; + this.locationFontTexture = locationFontTexture; + this.renderEngine = renderEngine; + + for (int i = 0; i < 64; i++) { + batchCommandPool.add(new FontDrawCmd()); + } + } + + // === Batched rendering + + private int batchDepth = 0; + + private int vtxWriterIndex = 0; + private int idxWriterIndex = 0; + private static final int INITIAL_BATCH_SIZE = 256; + private static final ResourceLocation DUMMY_RESOURCE_LOCATION = new ResourceLocation("angelica$dummy", + "this is invalid!"); + private FloatBuffer batchVtxPositions = BufferUtils.createFloatBuffer(INITIAL_BATCH_SIZE * 2); + private ByteBuffer batchVtxColors = BufferUtils.createByteBuffer(INITIAL_BATCH_SIZE * 4); + private FloatBuffer batchVtxTexCoords = BufferUtils.createFloatBuffer(INITIAL_BATCH_SIZE * 2); + private IntBuffer batchIndices = BufferUtils.createIntBuffer(INITIAL_BATCH_SIZE / 2 * 3); + private final ObjectArrayList batchCommands = ObjectArrayList.wrap(new FontDrawCmd[64], 0); + private final ObjectArrayList batchCommandPool = ObjectArrayList.wrap(new FontDrawCmd[64], 0); + + /** */ + private void pushVtx(float x, float y, int rgba, float u, float v) { + final int oldCap = batchVtxPositions.capacity() / 2; + if (vtxWriterIndex >= oldCap) { + final int newCap = oldCap * 2; + batchVtxPositions = CompatMemoryUtil.memReallocDirect(batchVtxPositions, newCap * 2); + batchVtxColors = CompatMemoryUtil.memReallocDirect(batchVtxColors, newCap * 4); + batchVtxTexCoords = CompatMemoryUtil.memReallocDirect(batchVtxTexCoords, newCap * 2); + final int oldIdxCap = batchIndices.capacity(); + final int newIdxCap = oldIdxCap * 2; + batchIndices = CompatMemoryUtil.memReallocDirect(batchIndices, newIdxCap); + } + final int idx = vtxWriterIndex; + final int idx2 = idx * 2; + final int idx4 = idx * 4; + batchVtxPositions.put(idx2, x); + batchVtxPositions.put(idx2 + 1, y); + // 0xAARRGGBB + batchVtxColors.put(idx4, (byte) ((rgba >> 16) & 0xFF)); + batchVtxColors.put(idx4 + 1, (byte) ((rgba >> 8) & 0xFF)); + batchVtxColors.put(idx4 + 2, (byte) (rgba & 0xFF)); + batchVtxColors.put(idx4 + 3, (byte) ((rgba >> 24) & 0xFF)); + batchVtxTexCoords.put(idx2, u); + batchVtxTexCoords.put(idx2 + 1, v); + vtxWriterIndex++; + } + + private void pushUntexRect(float x, float y, float w, float h, int rgba) { + final int vtxId = vtxWriterIndex; + pushVtx(x, y, rgba, 0, 0); + pushVtx(x, y + h, rgba, 0, 0); + pushVtx(x + w, y, rgba, 0, 0); + pushVtx(x + w, y + h, rgba, 0, 0); + pushQuadIdx(vtxId); + } + + private int pushQuadIdx(int startV) { + final int idx = idxWriterIndex; + batchIndices.put(idx, startV); + batchIndices.put(idx + 1, startV + 1); + batchIndices.put(idx + 2, startV + 2); + // + batchIndices.put(idx + 3, startV + 2); + batchIndices.put(idx + 4, startV + 1); + batchIndices.put(idx + 5, startV + 3); + idxWriterIndex += 6; + return idx; + } + + private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture) { + if (!batchCommands.isEmpty()) { + final FontDrawCmd lastCmd = batchCommands.get(batchCommands.size() - 1); + final int prevEndVtx = lastCmd.startVtx + lastCmd.idxCount; + if (prevEndVtx == startIdx && lastCmd.texture == texture) { + // Coalesce into one + lastCmd.idxCount += idxCount; + return; + } + } + if (batchCommandPool.isEmpty()) { + for (int i = 0; i < 64; i++) { + batchCommandPool.add(new FontDrawCmd()); + } + } + final FontDrawCmd cmd = batchCommandPool.pop(); + cmd.reset(startIdx, idxCount, texture); + batchCommands.add(cmd); + } + + private static final class FontDrawCmd { + + public int startVtx; + public int idxCount; + public ResourceLocation texture; + + public void reset(int startVtx, int vtxCount, ResourceLocation texture) { + this.startVtx = startVtx; + this.idxCount = vtxCount; + this.texture = texture; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (FontDrawCmd) obj; + return this.startVtx == that.startVtx && this.idxCount == that.idxCount && Objects.equals(this.texture, + that.texture); + } + + @Override + public int hashCode() { + return Objects.hash(startVtx, idxCount, texture); + } + + @Override + public String toString() { + return "FontDrawCmd[" + + "startVtx=" + + startVtx + + ", " + + "vtxCount=" + + idxCount + + ", " + + "texture=" + + texture + + ']'; + } + + public static final Comparator DRAW_ORDER_COMPARATOR = Comparator.comparing((FontDrawCmd fdc) -> fdc.texture, + Comparator.nullsLast(Comparator.comparing(ResourceLocation::getResourceDomain) + .thenComparing(ResourceLocation::getResourcePath))).thenComparing(fdc -> fdc.startVtx); + } + + /** + * Starts a new batch of font rendering operations. Can be called from within another batch with a matching end, to + * allow for easier optimizing of blocks of font rendering code. + */ + public void beginBatch() { + if (batchDepth == Integer.MAX_VALUE) { + throw new StackOverflowError("More than Integer.MAX_VALUE nested font rendering batch operations"); + } + batchDepth++; + } + + public void endBatch() { + if (batchDepth <= 0) { + batchDepth = 0; + return; + } + batchDepth--; + if (batchDepth == 0) { + // We finished any nested batches + flushBatch(); + } + } + + private void flushBatch() { + // Sort&Draw + batchCommands.sort(FontDrawCmd.DRAW_ORDER_COMPARATOR); + + ResourceLocation lastTexture = DUMMY_RESOURCE_LOCATION; + GLStateManager.enableTexture(); + GLStateManager.enableAlphaTest(); + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + GLStateManager.glShadeModel(GL_FLAT); + + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT); + glTexCoordPointer(2, 0, batchVtxTexCoords); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glColorPointer(4, GL_UNSIGNED_BYTE, 0, batchVtxColors); + glEnableClientState(GL_COLOR_ARRAY); + glVertexPointer(2, 0, batchVtxPositions); + glEnableClientState(GL_VERTEX_ARRAY); + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + // Use plain for loop to avoid allocations + final FontDrawCmd[] cmdsData = batchCommands.elements(); + final int cmdsSize = batchCommands.size(); + for (int i = 0; i < cmdsSize; i++) { + final FontDrawCmd cmd = cmdsData[i]; + if (!Objects.equals(lastTexture, cmd.texture)) { + if (lastTexture == null) { + GLStateManager.enableTexture(); + } else if (cmd.texture == null) { + GLStateManager.disableTexture(); + } + if (cmd.texture != null) { + ((FontRendererAccessor) (Object) underlying).angelica$bindTexture(cmd.texture); + } + lastTexture = cmd.texture; + } + batchIndices.limit(cmd.startVtx + cmd.idxCount); + batchIndices.position(cmd.startVtx); + glDrawElements(GL_TRIANGLES, batchIndices); + } + + glPopClientAttrib(); + + // Clear for the next batch + batchCommandPool.addAll(batchCommands); + batchCommands.clear(); + vtxWriterIndex = 0; + idxWriterIndex = 0; + batchIndices.limit(batchIndices.capacity()); + batchIndices.position(0); + } + + // === Actual text mesh generation + + public static boolean charInRange(char what, char fromInclusive, char toInclusive) { + return (what >= fromInclusive) && (what <= toInclusive); + } + + private ResourceLocation getUnicodePageLocation(int page) { + final ResourceLocation lookup = unicodePageLocations[page]; + if (lookup == null) { + final ResourceLocation rl = new ResourceLocation(String.format( + "textures/font/unicode_page_%02x.png", + page)); + unicodePageLocations[page] = rl; + return rl; + } else { + return lookup; + } + } + + private static final char FORMATTING_CHAR = 167; // § + + public float drawString(final float anchorX, final float anchorY, final int color, final boolean enableShadow, + final boolean unicodeFlag, final CharSequence string, int stringOffset, int stringLength) { + // noinspection SizeReplaceableByIsEmpty + if (string == null || string.length() == 0) { + return 0.0f; + } + final int shadowColor = (color & 0xfcfcfc) >> 2 | color & 0xff000000; + + this.beginBatch(); + float curX = anchorX; + try { + final int totalStringLength = string.length(); + stringOffset = MathHelper.clamp_int(stringOffset, 0, totalStringLength); + stringLength = MathHelper.clamp_int(stringLength, 0, totalStringLength - stringOffset); + if (stringLength <= 0) { + return 0; + } + final int stringEnd = stringOffset + stringLength; + + int curColor = color; + int curShadowColor = shadowColor; + boolean curItalic = false; + boolean curRandom = false; + boolean curBold = false; + boolean curStrikethrough = false; + boolean curUnderline = false; + + final float underlineY = anchorY + underlying.FONT_HEIGHT - 1.0f; + float underlineStartX = 0.0f; + float underlineEndX = 0.0f; + final float strikethroughY = anchorY + (float) (underlying.FONT_HEIGHT / 2); + float strikethroughStartX = 0.0f; + float strikethroughEndX = 0.0f; + + for (int charIdx = stringOffset; charIdx < stringEnd; charIdx++) { + char chr = string.charAt(charIdx); + if (chr == FORMATTING_CHAR && (charIdx + 1) < stringEnd) { + final char fmtCode = Character.toLowerCase(string.charAt(charIdx + 1)); + charIdx++; + + if (curUnderline && underlineStartX != underlineEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, 1.0f, curColor); + pushDrawCmd(ulIdx, 6, null); + underlineStartX = underlineEndX; + } + if (curStrikethrough && strikethroughStartX != strikethroughEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect( + strikethroughStartX, + strikethroughY, + strikethroughEndX - strikethroughStartX, + 1.0f, + curColor); + pushDrawCmd(ulIdx, 6, null); + strikethroughStartX = strikethroughEndX; + } + + final boolean is09 = charInRange(fmtCode, '0', '9'); + final boolean isAF = charInRange(fmtCode, 'a', 'f'); + if (is09 || isAF) { + curRandom = false; + curBold = false; + curStrikethrough = false; + curUnderline = false; + curItalic = false; + + final int colorIdx = is09 ? (fmtCode - '0') : (fmtCode - 'a' + 10); + final int rgb = this.colorCode[colorIdx]; + curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF); + final int shadowRgb = this.colorCode[colorIdx + 16]; + curShadowColor = (curShadowColor & 0xFF000000) | (shadowRgb & 0x00FFFFFF); + } else if (fmtCode == 'k') { + curRandom = true; + } else if (fmtCode == 'l') { + curBold = true; + } else if (fmtCode == 'm') { + curStrikethrough = true; + strikethroughStartX = curX - 1.0f; + strikethroughEndX = strikethroughStartX; + } else if (fmtCode == 'n') { + curUnderline = true; + underlineStartX = curX - 1.0f; + underlineEndX = underlineStartX; + } else if (fmtCode == 'o') { + curItalic = true; + } else if (fmtCode == 'r') { + curRandom = false; + curBold = false; + curStrikethrough = false; + curUnderline = false; + curItalic = false; + curColor = color; + curShadowColor = shadowColor; + } + + continue; + } + + int lutIndex = lookupMcFontPosition(chr); + if (curRandom && lutIndex != -1) { + int randomReplacementIndex; + do { + randomReplacementIndex = fontRandom.nextInt(this.charWidth.length); + } while (this.charWidth[lutIndex] != this.charWidth[randomReplacementIndex]); + + lutIndex = randomReplacementIndex; + chr = MCFONT_CHARS.charAt(lutIndex); + } + + final float shadowOffset = unicodeFlag ? 0.5F : 1.0F; + + // Check ASCII space, NBSP, NNBSP + if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') { + curX += 4; + continue; + } + + final float uStart; + final float vStart; + final float xAdvance; + final float glyphW; + final float uSz; + final float vSz; + final float itOff = curItalic ? 1.0F : 0.0F; // italic offset + final ResourceLocation texture; + + if (lutIndex == -1 || unicodeFlag) { + if (glyphWidth[chr] == 0) { + continue; + } + // Draw unicode char + final int uniPage = chr / 256; + texture = getUnicodePageLocation(uniPage); + final int startColumn = this.glyphWidth[chr] >>> 4; + final int endColumn = this.glyphWidth[chr] & 15; + final float startColumnF = (float) startColumn; + final float endColumnF = (float) (endColumn + 1); + uStart = ((float) (chr % 16 * 16) + startColumnF) / 256.0f; + vStart = ((float) ((chr & 255) / 16 * 16)) / 256.0f; + final float chrWidth = endColumnF - startColumnF - 0.02F; + glyphW = chrWidth / 2.0f + 1.0f; + xAdvance = (endColumnF - startColumnF) / 2.0F + 1.0F; + uSz = chrWidth / 256.0f; + vSz = 15.98f / 256.0f; + + } else { + // Draw "ASCII" char + uStart = ((lutIndex % 16) * 8) / 128.0F; + vStart = (float) ((lutIndex / 16) * 8) / 128.0F; + xAdvance = this.charWidth[lutIndex]; + if (xAdvance == 0) { + continue; + } + glyphW = xAdvance - 0.01F; + uSz = (glyphW - 1.0F) / 128.0F; + vSz = 7.99F / 128.0F; + texture = locationFontTexture; + } + + final int vtxId = vtxWriterIndex; + final int idxId = idxWriterIndex; + + int vtxCount = 0; + + if (enableShadow) { + pushVtx(curX + itOff + shadowOffset, anchorY + shadowOffset, curShadowColor, uStart, vStart); + pushVtx( + curX - itOff + shadowOffset, + anchorY + 7.99F + shadowOffset, + curShadowColor, + uStart, + vStart + vSz); + pushVtx(curX + glyphW - 1.0F + itOff + shadowOffset, + anchorY + shadowOffset, + curShadowColor, + uStart + uSz, + vStart); + pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset, + anchorY + 7.99F + shadowOffset, + curShadowColor, + uStart + uSz, + vStart + vSz); + pushQuadIdx(vtxId + vtxCount); + vtxCount += 4; + + if (curBold) { + final float shadowOffset2 = 2.0f * shadowOffset; + pushVtx(curX + itOff + shadowOffset2, anchorY + shadowOffset, curShadowColor, uStart, vStart); + pushVtx( + curX - itOff + shadowOffset2, + anchorY + 7.99F + shadowOffset, + curShadowColor, + uStart, + vStart + vSz); + pushVtx( + curX + glyphW - 1.0F + itOff + shadowOffset2, + anchorY + shadowOffset, + curShadowColor, + uStart + uSz, + vStart); + pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset2, + anchorY + 7.99F + shadowOffset, + curShadowColor, + uStart + uSz, + vStart + vSz); + pushQuadIdx(vtxId + vtxCount); + vtxCount += 4; + } + } + + pushVtx(curX + itOff, anchorY, curColor, uStart, vStart); + pushVtx(curX - itOff, anchorY + 7.99F, curColor, uStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F + itOff, anchorY, curColor, uStart + uSz, vStart); + pushVtx(curX + glyphW - 1.0F - itOff, anchorY + 7.99F, curColor, uStart + uSz, vStart + vSz); + pushQuadIdx(vtxId + vtxCount); + vtxCount += 4; + + if (curBold) { + pushVtx(shadowOffset + curX + itOff, anchorY, curColor, uStart, vStart); + pushVtx(shadowOffset + curX - itOff, anchorY + 7.99F, curColor, uStart, vStart + vSz); + pushVtx(shadowOffset + curX + glyphW - 1.0F + itOff, anchorY, curColor, uStart + uSz, vStart); + pushVtx(shadowOffset + curX + glyphW - 1.0F - itOff, + anchorY + 7.99F, + curColor, + uStart + uSz, + vStart + vSz); + pushQuadIdx(vtxId + vtxCount); + vtxCount += 4; + } + + pushDrawCmd(idxId, vtxCount / 2 * 3, texture); + curX += xAdvance + (curBold ? shadowOffset : 0.0f); + underlineEndX = curX; + strikethroughEndX = curX; + } + + if (curUnderline && underlineStartX != underlineEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, 1.0f, curColor); + pushDrawCmd(ulIdx, 6, null); + } + if (curStrikethrough && strikethroughStartX != strikethroughEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect( + strikethroughStartX, + strikethroughY, + strikethroughEndX - strikethroughStartX, + 1.0f, + curColor); + pushDrawCmd(ulIdx, 6, null); + } + + } finally { + this.endBatch(); + } + return curX; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/ExtendedBlockStorageExt.java b/src/main/java/com/gtnewhorizons/angelica/compat/ExtendedBlockStorageExt.java new file mode 100644 index 000000000..15993bc9d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/ExtendedBlockStorageExt.java @@ -0,0 +1,69 @@ +package com.gtnewhorizons.angelica.compat; + +import com.gtnewhorizons.angelica.AngelicaMod; +import com.gtnewhorizons.angelica.mixins.early.sodium.MixinExtendedBlockStorage; +import com.gtnewhorizons.angelica.mixins.interfaces.ExtendedNibbleArray; +import com.gtnewhorizons.neid.mixins.interfaces.IExtendedBlockStorageMixin; +import net.minecraft.world.chunk.NibbleArray; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import ru.fewizz.idextender.Hooks; + +public class ExtendedBlockStorageExt extends ExtendedBlockStorage { + public boolean hasSky; + + public ExtendedBlockStorageExt(int yBase, boolean hasSky) { + super(yBase, hasSky); + this.hasSky = hasSky; + } + + public ExtendedBlockStorageExt(ExtendedBlockStorage storage) { + super(((MixinExtendedBlockStorage) storage).getYBase(), storage.getSkylightArray() != null); + + int arrayLen; + if (AngelicaMod.isNEIDLoaded){ + final short[] block16BArray = ((IExtendedBlockStorageMixin)(Object)this).getBlock16BArray(); + System.arraycopy(((IExtendedBlockStorageMixin)(Object)storage).getBlock16BArray(), 0, block16BArray, 0, block16BArray.length); + if(storage.getBlockMSBArray() != null) { + this.setBlockMSBArray(new NibbleArray(block16BArray.length, 4)); + copyNibbleArray((ExtendedNibbleArray) storage.getBlockMSBArray(), (ExtendedNibbleArray) this.getBlockMSBArray()); + } + arrayLen = block16BArray.length; + } + else if (AngelicaMod.isOldNEIDLoaded){ + final short[] blockLSBArray = Hooks.get(this); + System.arraycopy(Hooks.get(storage), 0, blockLSBArray, 0, blockLSBArray.length); + // getBlockMSBArray is nuked in asm version + arrayLen = blockLSBArray.length; + } + else { + final byte[] blockLSBArray = this.getBlockLSBArray(); + System.arraycopy(storage.getBlockLSBArray(), 0, blockLSBArray, 0, blockLSBArray.length); + if(storage.getBlockMSBArray() != null) { + this.setBlockMSBArray(new NibbleArray(blockLSBArray.length, 4)); + copyNibbleArray((ExtendedNibbleArray) storage.getBlockMSBArray(), (ExtendedNibbleArray) this.getBlockMSBArray()); + } + arrayLen = blockLSBArray.length; + } + + + copyNibbleArray((ExtendedNibbleArray) storage.getMetadataArray(), (ExtendedNibbleArray)this.getMetadataArray()); + copyNibbleArray((ExtendedNibbleArray) storage.getBlocklightArray(), (ExtendedNibbleArray)this.getBlocklightArray()); + if(storage.getSkylightArray() != null) { + hasSky = true; + if(this.getSkylightArray() == null) { + this.setSkylightArray(new NibbleArray(arrayLen, 4)); + } + copyNibbleArray((ExtendedNibbleArray) storage.getSkylightArray(), (ExtendedNibbleArray) this.getSkylightArray()); + } + ((MixinExtendedBlockStorage) this).setBlockRefCount(((MixinExtendedBlockStorage) storage).getBlockRefCount()); + } + + + private static void copyNibbleArray(ExtendedNibbleArray srcArray, ExtendedNibbleArray dstArray) { + if (srcArray == null || dstArray == null) { + throw new RuntimeException("NibbleArray is null src: " + (srcArray == null) + " dst: " + (dstArray == null)); + } + final byte[] data = srcArray.getData(); + System.arraycopy(data, 0, dstArray.getData(), 0, data.length); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/lwjgl/CompatMemoryUtil.java b/src/main/java/com/gtnewhorizons/angelica/compat/lwjgl/CompatMemoryUtil.java new file mode 100644 index 000000000..6b311ca88 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/lwjgl/CompatMemoryUtil.java @@ -0,0 +1,102 @@ +package com.gtnewhorizons.angelica.compat.lwjgl; + +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class CompatMemoryUtil { + public static ByteBuffer memReallocDirect(ByteBuffer old, int capacity) { + ByteBuffer newBuf = BufferUtils.createByteBuffer(capacity); + int oldPos = old.position(); + old.rewind(); + newBuf.put(old); + newBuf.position(Math.min(capacity, oldPos)); + return newBuf; + } + + public static IntBuffer memReallocDirect(IntBuffer old, int capacity) { + IntBuffer newBuf = BufferUtils.createIntBuffer(capacity); + int oldPos = old.position(); + old.rewind(); + newBuf.put(old); + newBuf.position(Math.min(capacity, oldPos)); + return newBuf; + } + + public static FloatBuffer memReallocDirect(FloatBuffer old, int capacity) { + FloatBuffer newBuf = BufferUtils.createFloatBuffer(capacity); + int oldPos = old.position(); + old.rewind(); + newBuf.put(old); + newBuf.position(Math.min(capacity, oldPos)); + return newBuf; + } + + /** + * Backported from LWJGL3 under the BSD 3-Clause "New" or "Revised" License license + * + *

This class provides functionality for managing native memory. + * + *

All methods in this class will make use of {@link sun.misc.Unsafe} if it's available, for performance. If Unsafe is not available, the fallback + * implementations make use of reflection and, in the worst-case, JNI.

+ * + *

Method names in this class are prefixed with {@code mem} to avoid ambiguities when used with static imports.

+ */ + + static final sun.misc.Unsafe UNSAFE; + + static { + UNSAFE = getUnsafeInstance(); + } + + private static sun.misc.Unsafe getUnsafeInstance() { + java.lang.reflect.Field[] fields = sun.misc.Unsafe.class.getDeclaredFields(); + + /* + Different runtimes use different names for the Unsafe singleton, + so we cannot use .getDeclaredField and we scan instead. For example: + + Oracle: theUnsafe + PERC : m_unsafe_instance + Android: THE_ONE + */ + for (java.lang.reflect.Field field : fields) { + if (!field.getType().equals(sun.misc.Unsafe.class)) { + continue; + } + + int modifiers = field.getModifiers(); + if (!(java.lang.reflect.Modifier.isStatic(modifiers) && java.lang.reflect.Modifier.isFinal(modifiers))) { + continue; + } + + try { + field.setAccessible(true); + return (sun.misc.Unsafe)field.get(null); + } catch (Exception ignored) { + } + break; + } + + throw new UnsupportedOperationException("LWJGL requires sun.misc.Unsafe to be available."); + } + + + public static void memPutByte(long ptr, byte value) { UNSAFE.putByte(null, ptr, value); } + public static void memPutShort(long ptr, short value) { UNSAFE.putShort(null, ptr, value); } + public static void memPutInt(long ptr, int value) { UNSAFE.putInt(null, ptr, value); } + public static void memPutLong(long ptr, long value) { UNSAFE.putLong(null, ptr, value); } + public static void memPutFloat(long ptr, float value) { UNSAFE.putFloat(null, ptr, value); } + public static void memPutDouble(long ptr, double value) { UNSAFE.putDouble(null, ptr, value); } + + public static boolean memGetBoolean(long ptr) { return UNSAFE.getByte(null, ptr) != 0; } + public static byte memGetByte(long ptr) { return UNSAFE.getByte(null, ptr); } + public static short memGetShort(long ptr) { return UNSAFE.getShort(null, ptr); } + public static int memGetInt(long ptr) { return UNSAFE.getInt(null, ptr); } + public static long memGetLong(long ptr) { return UNSAFE.getLong(null, ptr); } + public static float memGetFloat(long ptr) { return UNSAFE.getFloat(null, ptr); } + public static double memGetDouble(long ptr) { return UNSAFE.getDouble(null, ptr); } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/AutoClosableAbstractTexture.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/AutoClosableAbstractTexture.java new file mode 100644 index 000000000..ba5648877 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/AutoClosableAbstractTexture.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.lwjgl.opengl.GL11; + +public abstract class AutoClosableAbstractTexture extends AbstractTexture implements AutoCloseable { + @Override + public void close() throws Exception {} + + // TODO: Is this needed? + public void bind() { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, getGlTextureId()); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Axis.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Axis.java new file mode 100644 index 000000000..d69400da9 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Axis.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; + +public enum Axis { + X, + Y, + Z; + + public static Axis fromDirection(ModelQuadFacing dir) { + return switch (dir) { + case DOWN, UP -> Y; + case NORTH, SOUTH -> Z; + case WEST, EAST -> X; + default -> null; + }; + + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/BlockPos.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/BlockPos.java new file mode 100644 index 000000000..0536347e6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/BlockPos.java @@ -0,0 +1,99 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import net.minecraft.util.MathHelper; +import net.minecraft.world.ChunkPosition; +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3i; + +import java.math.RoundingMode; + +import static com.google.common.math.IntMath.log2; + +// Should we keep this? +public class BlockPos extends Vector3i { + + private static final int SIZE_BITS_X = 1 + log2(MathHelper.roundUpToPowerOfTwo(30000000), RoundingMode.UNNECESSARY); + private static final int SIZE_BITS_Z = SIZE_BITS_X; + private static final int SIZE_BITS_Y = 64 - SIZE_BITS_X - SIZE_BITS_Z; + private static final long BITS_X = (1L << SIZE_BITS_X) - 1L; + private static final long BITS_Y = (1L << SIZE_BITS_Y) - 1L; + private static final long BITS_Z = (1L << SIZE_BITS_Z) - 1L; + private static final int BIT_SHIFT_Z = SIZE_BITS_Y; + private static final int BIT_SHIFT_X = SIZE_BITS_Y + SIZE_BITS_Z; + + public BlockPos() { + super(); + } + public BlockPos(int x, int y, int z) { + super(x, y, z); + } + + public BlockPos(ChunkPosition chunkPosition) { + super(chunkPosition.chunkPosX, chunkPosition.chunkPosY, chunkPosition.chunkPosZ); + } + + public int getX() { + return this.x; + } + public int getY() { + return this.y; + } + public int getZ() { + return this.z; + } + + public BlockPos set(int x, int y, int z) { + super.set(x, y, z); + return this; + } + + /** + * This method does NOT mutate the BlockPos + */ + public BlockPos offset(ForgeDirection d) { + return new BlockPos(this.x + d.offsetX, this.y + d.offsetY, this.z + d.offsetZ); + } + + /** + * This method does NOT mutate the BlockPos + */ + public BlockPos down() { + return offset(ForgeDirection.DOWN); + } + + /** + * This method does NOT mutate the BlockPos + */ + public BlockPos up() { + return offset(ForgeDirection.UP); + } + + public long asLong() { + return asLong(this.x, this.y, this.z); + } + + public static long asLong(int x, int y, int z) { + long l = 0L; + l |= ((long)x & BITS_X) << BIT_SHIFT_X; + l |= ((long)y & BITS_Y) << 0; + l |= ((long)z & BITS_Z) << BIT_SHIFT_Z; + return l; + } + + + public static int unpackLongX(long packedPos) { + return (int)(packedPos << 64 - BIT_SHIFT_X - SIZE_BITS_X >> 64 - SIZE_BITS_X); + } + + public static int unpackLongY(long packedPos) { + return (int)(packedPos << 64 - SIZE_BITS_Y >> 64 - SIZE_BITS_Y); + } + + public static int unpackLongZ(long packedPos) { + return (int)(packedPos << 64 - BIT_SHIFT_Z - SIZE_BITS_Z >> 64 - SIZE_BITS_Z); + } + + public static class Mutable extends BlockPos { + + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ByteBufferBackedInputStream.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ByteBufferBackedInputStream.java new file mode 100644 index 000000000..e0b61f1ed --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ByteBufferBackedInputStream.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + + +public class ByteBufferBackedInputStream extends InputStream { + + ByteBuffer buf; + + public ByteBufferBackedInputStream(ByteBuffer buf) { + this.buf = buf; + } + + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + @Override + public int read(byte @NotNull [] bytes, int off, int len) throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Camera.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Camera.java new file mode 100644 index 000000000..07f8c0297 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Camera.java @@ -0,0 +1,40 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import com.gtnewhorizons.angelica.rendering.RenderingState; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.EntityLivingBase; +import org.joml.Matrix4f; +import org.joml.Vector3d; +import org.joml.Vector4f; + +@Getter +public class Camera { + final Vector3d pos = new Vector3d(); + final BlockPos.Mutable blockPos = new BlockPos.Mutable(); + float pitch; + float yaw; + EntityLivingBase entity; + boolean thirdPerson; + final float partialTicks; + + public Camera(EntityLivingBase entity, float partialTicks) { + this.partialTicks = partialTicks; + final Vector4f offset = new Vector4f(); // third person offset + final Matrix4f inverseModelView = new Matrix4f(RenderingState.INSTANCE.getModelViewMatrix()).invert(); + inverseModelView.transform(offset); + + final double camX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks + offset.x; + final double camY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks + offset.y; + final double camZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks + offset.z; + this.entity = entity; + + pos.set(camX, camY, camZ); + blockPos.set((int)entity.posX, (int)entity.posY, (int)entity.posZ); + pitch = entity.cameraPitch; + yaw = entity.rotationYaw; + thirdPerson = Minecraft.getMinecraft().gameSettings.thirdPersonView == 1; + + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionData.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionData.java new file mode 100644 index 000000000..4ed46a3d5 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionData.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import net.minecraftforge.common.util.ForgeDirection; + +import java.util.BitSet; +import java.util.Set; + +public class ChunkOcclusionData { + private static final int DIRECTION_COUNT = ForgeDirection.values().length; + private final BitSet visibility; + + public ChunkOcclusionData() { + this.visibility = new BitSet(DIRECTION_COUNT * DIRECTION_COUNT); + } + + public void addOpenEdgeFaces(Set faces) { + for (ForgeDirection dirFrom : faces) { + for (ForgeDirection dirTo : faces) { + this.setVisibleThrough(dirFrom, dirTo, true); + } + } + + for (ForgeDirection direction : faces) { + this.visibility.set(direction.ordinal() * DIRECTION_COUNT + direction.ordinal()); + } + } + public void setVisibleThrough(ForgeDirection from, ForgeDirection to, boolean visible) { + this.visibility.set(from.ordinal() + to.ordinal() * DIRECTION_COUNT, visible); + this.visibility.set(to.ordinal() + from.ordinal() * DIRECTION_COUNT, visible); + } + + public boolean isVisibleThrough(ForgeDirection from, ForgeDirection to) { + return this.visibility.get(from.ordinal() + to.ordinal() * DIRECTION_COUNT); + } + + public void fill(boolean visible) { + this.visibility.set(0, this.visibility.size(), visible); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionDataBuilder.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionDataBuilder.java new file mode 100644 index 000000000..e4e1eb7a4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkOcclusionDataBuilder.java @@ -0,0 +1,155 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.ints.IntPriorityQueue; +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import net.minecraftforge.common.util.ForgeDirection; + +import java.util.BitSet; +import java.util.EnumSet; +import java.util.Set; + +/** + * WARNING: Minecraft 1.16 code rip!! + */ + +public class ChunkOcclusionDataBuilder { + private static final int STEP_X = (int)Math.pow(16.0, 0.0); + private static final int STEP_Z = (int)Math.pow(16.0, 1.0); + private static final int STEP_Y = (int)Math.pow(16.0, 2.0); + private static final ForgeDirection[] DIRECTIONS = DirectionUtil.ALL_DIRECTIONS; + private final BitSet closed = new BitSet(4096); + private static final int[] EDGE_POINTS = new int[1352]; + + static { + int k = 0; + + for(int l = 0; l < 16; ++l) { + for(int m = 0; m < 16; ++m) { + for(int n = 0; n < 16; ++n) { + if (l == 0 || l == 15 || m == 0 || m == 15 || n == 0 || n == 15) { + EDGE_POINTS[k++] = pack(l, m, n); + } + } + } + } + } + private int openCount = 4096; + + public void markClosed(BlockPos pos) { + this.closed.set(pack(pos), true); + --this.openCount; + } + + private static int pack(BlockPos pos) { + return pack(pos.getX() & 15, pos.getY() & 15, pos.getZ() & 15); + } + + private static int pack(int x, int y, int z) { + return x << 0 | y << 8 | z << 4; + } + + public ChunkOcclusionData build() { + final ChunkOcclusionData lv = new ChunkOcclusionData(); + if (4096 - this.openCount < 256) { + lv.fill(true); + } else if (this.openCount == 0) { + lv.fill(false); + } else { + for(int i : EDGE_POINTS) { + if (!this.closed.get(i)) { + lv.addOpenEdgeFaces(this.getOpenFaces(i)); + } + } + } + + return lv; + } + + private Set getOpenFaces(int pos) { + final Set set = EnumSet.noneOf(ForgeDirection.class); + final IntPriorityQueue intPriorityQueue = new IntArrayFIFOQueue(); + intPriorityQueue.enqueue(pos); + this.closed.set(pos, true); + + while(!intPriorityQueue.isEmpty()) { + final int j = intPriorityQueue.dequeueInt(); + this.addEdgeFaces(j, set); + + for(ForgeDirection lv : DIRECTIONS) { + final int k = this.offset(j, lv); + if (k >= 0 && !this.closed.get(k)) { + this.closed.set(k, true); + intPriorityQueue.enqueue(k); + } + } + } + + return set; + } + + private void addEdgeFaces(int pos, Set openFaces) { + final int j = pos >> 0 & 15; + if (j == 0) { + openFaces.add(ForgeDirection.WEST); + } else if (j == 15) { + openFaces.add(ForgeDirection.EAST); + } + + final int k = pos >> 8 & 15; + if (k == 0) { + openFaces.add(ForgeDirection.DOWN); + } else if (k == 15) { + openFaces.add(ForgeDirection.UP); + } + + final int l = pos >> 4 & 15; + if (l == 0) { + openFaces.add(ForgeDirection.NORTH); + } else if (l == 15) { + openFaces.add(ForgeDirection.SOUTH); + } + } + + private int offset(int pos, ForgeDirection arg) { + return switch (arg) { + case DOWN -> { + if ((pos >> 8 & 15) == 0) { + yield -1; + } + yield pos - STEP_Y; + } + case UP -> { + if ((pos >> 8 & 15) == 15) { + yield -1; + } + yield pos + STEP_Y; + } + case NORTH -> { + if ((pos >> 4 & 15) == 0) { + yield -1; + } + yield pos - STEP_Z; + } + case SOUTH -> { + if ((pos >> 4 & 15) == 15) { + yield -1; + } + yield pos + STEP_Z; + } + case WEST -> { + if ((pos >> 0 & 15) == 0) { + yield -1; + } + yield pos - STEP_X; + } + case EAST -> { + if ((pos >> 0 & 15) == 15) { + yield -1; + } + yield pos + STEP_X; + } + default -> -1; + }; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkPos.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkPos.java new file mode 100644 index 000000000..95813a5f8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkPos.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +// See if we can merge/mixin/extend ChunkCoordIntPair? +public class ChunkPos { + public static long INT_MASK = (1L << Integer.SIZE) - 1; + + public final int x; + public final int z; + + public ChunkPos(int x, int z) { + this.x = x; + this.z = z; + } + + public ChunkPos(BlockPos pos) { + this.x = pos.getX() >> 4; + this.z = pos.getZ() >> 4; + } + + public ChunkPos(long pos) { + this.x = (int)pos; + this.z = (int)(pos >> 32); + } + + public static int getPackedX(long pos) { + return (int)(pos & INT_MASK); + } + + public static int getPackedZ(long pos) { + return (int)(pos >>> 32 & INT_MASK); + } + + public long toLong() { + return toLong(this.x, this.z); + } + + public static long toLong(int x, int z) { + return (long)x & 4294967295L | ((long)z & 4294967295L) << 32; + } + + public int hashCode() + { + final int i = 1664525 * this.x + 1013904223; + final int j = 1664525 * (this.z ^ -559038737) + 1013904223; + return i ^ j; + } + + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof ChunkPos lv) { + return this.x == lv.x && this.z == lv.z; + } + return false; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkSectionPos.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkSectionPos.java new file mode 100644 index 000000000..cedd9292b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/ChunkSectionPos.java @@ -0,0 +1,88 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import org.joml.Vector3i; + +// See if we can merge/mixin/extend ChunkPosition maybe? +public class ChunkSectionPos extends Vector3i { + + public static int getSectionCoord(int coord) { + return coord >> 4; + } + public static int getBlockCoord(int sectionCoord) { + return sectionCoord << 4; + } + + private ChunkSectionPos(int x, int y, int z) { + super(x, y, z); + } + public static ChunkSectionPos from(int x, int y, int z) { + return new ChunkSectionPos(x, y, z); + } + + public static ChunkSectionPos from(BlockPos pos) { + return new ChunkSectionPos(getSectionCoord(pos.getX()), getSectionCoord(pos.getY()), getSectionCoord(pos.getZ())); + } + + public static long asLong(int x, int y, int z) { + long l = 0L; + l |= ((long)x & 4194303L) << 42; + l |= ((long)y & 1048575L) << 0; + l |= ((long)z & 4194303L) << 20; + return l; + } + + public static int getLocalCoord(int coord) { + return coord & 15; + } + + public static short packLocal(BlockPos pos) { + int i = getLocalCoord(pos.x); + int j = getLocalCoord(pos.y); + int k = getLocalCoord(pos.z); + return (short)(i << 8 | k << 4 | j << 0); + } + + public long asLong() { + return asLong(this.x, this.y, this.z); + } + + public int getSectionX() { + return this.x; + } + + public int getSectionY() { + return this.y; + } + + public int getSectionZ() { + return this.z; + } + + public int getMinX() { + return this.x << 4; + } + + public int getMinY() { + return this.y << 4; + } + + public int getMinZ() { + return this.z << 4; + } + + public int getMaxX() { + return (this.x << 4) + 15; + } + + public int getMaxY() { + return (this.y << 4) + 15; + } + + public int getMaxZ() { + return (this.z << 4) + 15; + } + + public ChunkPos toChunkPos() { + return new ChunkPos(this.getSectionX(), this.getSectionZ()); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/CompatMathHelper.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/CompatMathHelper.java new file mode 100644 index 000000000..1808502a3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/CompatMathHelper.java @@ -0,0 +1,27 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +public class CompatMathHelper { + public static int smallestEncompassingPowerOfTwo(int value) { + int j = value - 1; + j |= j >> 1; + j |= j >> 2; + j |= j >> 4; + j |= j >> 8; + j |= j >> 16; + return j + 1; + } + public static int roundUpToMultiple(int value, int divisor) { + if (divisor == 0) { + return 0; + } else if (value == 0) { + return divisor; + } else { + if (value < 0) { + divisor *= -1; + } + + int k = value % divisor; + return k == 0 ? value : value + divisor - k; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Constants.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Constants.java new file mode 100644 index 000000000..e66a880c4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/Constants.java @@ -0,0 +1,5 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +public class Constants { + public static final float DEGREES_TO_RADIANS = (float)Math.PI / 180.0F; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/InteractionHand.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/InteractionHand.java new file mode 100644 index 000000000..98618563c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/InteractionHand.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.compat.mojang; + + +public enum InteractionHand { + MAIN_HAND, + OFF_HAND; + + InteractionHand() { + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/NativeImage.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/NativeImage.java new file mode 100644 index 000000000..03ddd3c51 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/NativeImage.java @@ -0,0 +1,121 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import lombok.Getter; +import net.coderbot.iris.Iris; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +// TBD +public class NativeImage extends BufferedImage { + @Getter + private final Format format; + @Getter + private final int width; + @Getter + private final int height; + @Getter + private final int size; + + public NativeImage(int width, int height, boolean useStb) { + this(NativeImage.Format.RGBA, width, height, useStb); + } + + public NativeImage(Format format, int width, int height, boolean useStb) { + super(width, height, BufferedImage.TYPE_INT_ARGB); + this.format = format; + this.width = width; + this.height = height; + this.size = width * height * format.components; + } + public NativeImage(Format format, int width, int height, BufferedImage image) { + super(image.getColorModel(), image.getRaster(), image.isAlphaPremultiplied(), null); + this.format = format; + this.width = width; + this.height = height; + this.size = width * height * format.components; + } + + public static NativeImage read(ByteBuffer buf) throws IOException { + return read(new ByteBufferBackedInputStream(buf)); + } + public static NativeImage read(InputStream inputStream) throws IOException { + BufferedImage image = ImageIO.read(inputStream); + return new NativeImage(Format.RGBA, image.getWidth(), image.getHeight(), image); + } + + public void downloadTexture(int level, boolean bl) { +// this.checkAllocated(); + GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, format.components); +// GlStateManager._getTexImage(3553, level, format.glFormat, GL11.GL_UNSIGNED_BYTE, this.pixels); + +// final int width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_WIDTH); +// final int height = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_HEIGHT); + IntBuffer buffer = BufferUtils.createIntBuffer(size); + int[] data = new int[size]; + + GL11.glGetTexImage(GL11.GL_TEXTURE_2D, level, format.glFormat, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, buffer); + buffer.get(data); + setRGB(0, 0, width, height, data, 0, width); + } + public void writeToFile(File file) throws IOException{ + try { + ImageIO.write(this, "png", file); + } catch(IOException ioexception) { + Iris.logger.info("[TextureDump] Unable to write: ", ioexception); + } + } + public static int combine(int i, int j, int k, int l) { + return (i & 255) << 24 | (j & 255) << 16 | (k & 255) << 8 | (l & 255); + } + + public static int getA(int i) { + return i >> 24 & 255; + } + + public static int getR(int i) { + return i >> 0 & 255; + } + + public static int getG(int i) { + return i >> 8 & 255; + } + + public static int getB(int i) { + return i >> 16 & 255; + } + + public int getPixelRGBA(int x, int y) { + return getRGB(x, y); + } + + public void setPixelRGBA(int x, int y, int rgb) { + setRGB(x, y, rgb); + } + + + public enum Format { + RGBA(4, GL11.GL_RGBA, BufferedImage.TYPE_INT_ARGB), + RGB(3, GL11.GL_RGB, BufferedImage.TYPE_INT_RGB); + + private final int components; + private final int glFormat; + private final int imageType; + + + Format(int components, int glFormat, int imageType) { + this.components = components; + this.glFormat = glFormat; + this.imageType = imageType; + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexBuffer.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexBuffer.java new file mode 100644 index 000000000..a72452e39 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexBuffer.java @@ -0,0 +1,48 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + + +public class VertexBuffer implements AutoCloseable { + private int id; + private int vertexCount; + + public VertexBuffer() { + this.id = GL15.glGenBuffers(); + } + + public void bind() { + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.id); + } + + public void unbind() { + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + } + + public void upload(ByteBuffer buffer, int vertexCount) { + if (this.id == -1) return; + this.vertexCount = vertexCount; + this.bind(); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW); + this.unbind(); + } + + public void close() { + if (this.id >= 0) { + GL15.glDeleteBuffers(this.id); + this.id = -1; + } + } + + public void draw(FloatBuffer floatBuffer, int mode) { + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + GL11.glMultMatrix(floatBuffer); + GL11.glDrawArrays(mode, 0, this.vertexCount); + GL11.glPopMatrix(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexFormatElement.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexFormatElement.java new file mode 100644 index 000000000..7ba91e0eb --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/VertexFormatElement.java @@ -0,0 +1,107 @@ +package com.gtnewhorizons.angelica.compat.mojang; + +import lombok.Getter; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.util.function.IntConsumer; + + +@Getter +public class VertexFormatElement { + protected final Type type; + protected final Usage usage; + protected final int index; + protected final int count; + protected final int byteSize; + + public VertexFormatElement(int index, Type type, Usage usage, int count) { + this.index = index; + this.type = type; + this.usage = usage; + this.count = count; + this.byteSize = type.getSize() * count; + } + + public void setupBufferState(long l, int i) { + this.usage.setupBufferState(this.count, this.type.getGlType(), i, l, this.index); + } + + public void clearBufferState() { + this.usage.clearBufferState(this.index); + } + + public enum Usage { + POSITION("Position", (i, j, k, l, m) -> { + GL11.glVertexPointer(i, j, k, l); + GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY); + }, (i) -> GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY)), + NORMAL("Normal", (i, j, k, l, m) -> { + GL11.glNormalPointer(j, k, l); + GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY); + }, (i) -> GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY)), + COLOR("Vertex Color", (i, j, k, l, m) -> { + GL11.glColorPointer(i, j, k, l); + GL11.glEnableClientState(GL11.GL_COLOR_ARRAY); + }, i -> GL11.glDisableClientState(GL11.GL_COLOR_ARRAY)), + UV("UV", (i, j, k, l, m) -> { + GL13.glClientActiveTexture(GL13.GL_TEXTURE0 + m); + GL11.glTexCoordPointer(i, j, k, l); + GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY); + GL13.glClientActiveTexture(GL13.GL_TEXTURE0); + }, i -> { + GL13.glClientActiveTexture(GL13.GL_TEXTURE0 + i); + GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY); + GL13.glClientActiveTexture(GL13.GL_TEXTURE0); + }), + PADDING("Padding", (i, j, k, l, m) -> {}, i-> {}), + GENERIC("Generic", (i, j, k, l, m) -> { + GL20.glEnableVertexAttribArray(m); + GL20.glVertexAttribPointer(m, i, j, false, k, l); + }, GL20::glDisableVertexAttribArray); + + @Getter private final String name; + private final SetupState setupState; + private final IntConsumer clearState; + + Usage(String name, SetupState setupState, IntConsumer clearState) { + this.name = name; + this.setupState = setupState; + this.clearState = clearState; + } + + private void setupBufferState(int i, int j, int k, long l, int m) { + this.setupState.setupBufferState(i, j, k, l, m); + } + + public void clearBufferState(int i) { + this.clearState.accept(i); + } + + interface SetupState { + void setupBufferState(int i, int j, int k, long l, int m); + } + } + + @Getter + public enum Type { + FLOAT(4, "Float", GL11.GL_FLOAT), + UBYTE(1, "Unsigned Byte", GL11.GL_UNSIGNED_BYTE), + BYTE(1, "Byte", GL11.GL_BYTE), + USHORT(2, "Unsigned Short", GL11.GL_UNSIGNED_SHORT), + SHORT(2, "Short", GL11.GL_SHORT), + UINT(4, "Unsigned Int", GL11.GL_UNSIGNED_INT), + INT(4, "Int", GL11.GL_INT); + + private final int size; + private final String name; + private final int glType; + + Type(int size, String name, int glType) { + this.size = size; + this.name = name; + this.glType = glType; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/nd/BufferWriter.java b/src/main/java/com/gtnewhorizons/angelica/compat/nd/BufferWriter.java new file mode 100644 index 000000000..9d5681669 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/nd/BufferWriter.java @@ -0,0 +1,63 @@ +package com.gtnewhorizons.angelica.compat.nd; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +public class BufferWriter { + // Adapted from Neodymium + + private final ByteBuffer buf; + + private final FloatBuffer floatBuffer; + private final ShortBuffer shortBuffer; + private final IntBuffer intBuffer; + + public BufferWriter(ByteBuffer buf) { + this.buf = buf; + this.floatBuffer = buf.asFloatBuffer(); + this.shortBuffer = buf.asShortBuffer(); + this.intBuffer = buf.asIntBuffer(); + } + + private void incrementPosition(int add) { + buf.position(buf.position() + add); + floatBuffer.position(buf.position() / 4); + shortBuffer.position(buf.position() / 2); + intBuffer.position(buf.position() / 4); + } + + public void writeFloat(float x) { + try { + floatBuffer.put(x); + + incrementPosition(4); + } catch(Exception e){ + e.printStackTrace(); + } + } + + public void writeInt(int x) { + intBuffer.put(x); + + incrementPosition(4); + } + + public void writeByte(byte x) { + buf.put(x); // this increments the buffer position by 1 + + incrementPosition(0); + } + + public int position() { + return buf.position(); + } + + public void writeShort(short s) { + shortBuffer.put(s); + + incrementPosition(2); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/nd/Quad.java b/src/main/java/com/gtnewhorizons/angelica/compat/nd/Quad.java new file mode 100644 index 000000000..7d155a736 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/nd/Quad.java @@ -0,0 +1,337 @@ +package com.gtnewhorizons.angelica.compat.nd; + +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadView; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.pipeline.BlockRenderer; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3f; +import org.lwjgl.opengl.GL11; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; + +public class Quad implements ModelQuadView { + // Adapted from Neodymium + + private final static int DEFAULT_BRIGHTNESS = 15 << 20 | 15 << 4; + private final static int DEFAULT_COLOR = 0xFFFFFFFF; + + public float[] xs = new float[4]; + public float[] ys = new float[4]; + public float[] zs = new float[4]; + public float minX = Float.POSITIVE_INFINITY; + public float minY = Float.POSITIVE_INFINITY; + public float minZ = Float.POSITIVE_INFINITY; + public float maxX = Float.NEGATIVE_INFINITY; + public float maxY = Float.NEGATIVE_INFINITY; + public float maxZ = Float.NEGATIVE_INFINITY; + public float[] us = new float[4]; + public float[] vs = new float[4]; + public int[] bs = new int[4]; + public int[] cs = new int[4]; + // TODO normals? + public boolean deleted; + + public ModelQuadFacing normal; + public int offset; + public BlockRenderer.Flags flags; + + // Is positive U direction parallel to edge 0-1? + public boolean uDirectionIs01; + + public boolean isRectangle; + + // 0: quads glued together on edge 1-2 or 3-0 ("megaquad row length") + // 1: quads glued together on edge 0-1 or 2-3 ("megaquad column length") + private final int[] quadCountByDirection = {1, 1}; + + private final Vector3f vectorA = new Vector3f(), vectorB = new Vector3f(), vectorC = new Vector3f(); + + private boolean hasColor; + private boolean hasShade; + + public boolean hasColor() { + return this.hasColor; + } + + public int[] getColors() { + return this.cs; + } + + public boolean hasShade() { + return this.hasShade; + } + + public ForgeDirection getFace() { + // TODO: Sodium/Quad Facing + return ForgeDirection.UP; + } + + @Override + public float getX(int idx) { + return xs[idx]; + } + + @Override + public float getY(int idx) { + return ys[idx]; + } + + @Override + public float getZ(int idx) { + return zs[idx]; + } + + @Override + public int getColor(int idx) { + return cs[idx]; + } + + @Override + public float getTexU(int idx) { + return us[idx]; + } + + @Override + public float getTexV(int idx) { + return vs[idx]; + } + + @Override + public int getFlags() { + return 0; + } + + @Override + public int getLight(int idx) { + return bs[idx]; + } + + @Override + public int getNormal(int idx) { + return 0; + } + + @Override + public int getColorIndex() { + return 0; + } + + @Override + public TextureAtlasSprite rubidium$getSprite() { + return null; + } + + private void read(int[] rawBuffer, int offset, float offsetX, float offsetY, float offsetZ, int drawMode, BlockRenderer.Flags flags) { + int vertices = drawMode == GL11.GL_TRIANGLES ? 3 : 4; + for(int vi = 0; vi < vertices; vi++) { + int i = offset + vi * 8; + + xs[vi] = Float.intBitsToFloat(rawBuffer[i + 0]) + offsetX; + ys[vi] = Float.intBitsToFloat(rawBuffer[i + 1]) + offsetY; + zs[vi] = Float.intBitsToFloat(rawBuffer[i + 2]) + offsetZ; + + us[vi] = Float.intBitsToFloat(rawBuffer[i + 3]); + vs[vi] = Float.intBitsToFloat(rawBuffer[i + 4]); + + bs[vi] = flags.hasBrightness ? rawBuffer[i + 7] : DEFAULT_BRIGHTNESS; + cs[vi] = flags.hasColor ? rawBuffer[i + 5] : DEFAULT_COLOR; + this.hasColor |= flags.hasColor; + this.hasShade |= flags.hasBrightness; + + i += 8; + } + + if(vertices == 3) { + // Quadrangulate! + xs[3] = xs[2]; + ys[3] = ys[2]; + zs[3] = zs[2]; + + us[3] = us[2]; + vs[3] = vs[2]; + + bs[3] = bs[2]; + cs[3] = cs[2]; + } + } + + public void setState(int[] rawBuffer, int offset, BlockRenderer.Flags flags, int drawMode, float offsetX, float offsetY, float offsetZ) { + resetState(); + + read(rawBuffer, offset, offsetX, offsetY, offsetZ, drawMode, flags); + + if(xs[0] == xs[1] && xs[1] == xs[2] && xs[2] == xs[3] && ys[0] == ys[1] && ys[1] == ys[2] && ys[2] == ys[3]) { + // ignore empty quads (e.g. alpha pass of EnderIO item conduits) + deleted = true; + return; + } + + uDirectionIs01 = us[0] != us[1]; + + updateMinMaxXYZ(); + updateIsRectangle(); + + vectorA.set(xs[1] - xs[0], ys[1] - ys[0], zs[1] - zs[0]); + vectorB.set(xs[2] - xs[1], ys[2] - ys[1], zs[2] - zs[1]); + vectorA.cross(vectorB, vectorC); + + normal = ModelQuadFacing.fromVector(vectorC); + } + + private void resetState() { + Arrays.fill(xs, 0); + Arrays.fill(ys, 0); + Arrays.fill(zs, 0); + Arrays.fill(us, 0); + Arrays.fill(vs, 0); + Arrays.fill(bs, 0); + Arrays.fill(cs, 0); + + minX = Float.POSITIVE_INFINITY; + minY = Float.POSITIVE_INFINITY; + minZ = Float.POSITIVE_INFINITY; + maxX = Float.NEGATIVE_INFINITY; + maxY = Float.NEGATIVE_INFINITY; + maxZ = Float.NEGATIVE_INFINITY; + + deleted = false; + normal = null; + offset = 0; + flags = null; + uDirectionIs01 = false; + Arrays.fill(quadCountByDirection, 1); + } + + public void writeToBuffer(BufferWriter out) throws IOException { + for(int vertexI = 0; vertexI < 4; vertexI++) { + int vi = vertexI; + int provokingI = 3; + + float x = xs[vi]; + float y = ys[vi]; + float z = zs[vi]; + + out.writeFloat(x); + out.writeFloat(y); + out.writeFloat(z); + + float u = us[vi]; + float v = vs[vi]; + + out.writeFloat(u); + out.writeFloat(v); + + int b = bs[vi]; + + out.writeInt(b); + + int c = cs[vi]; + + out.writeInt(c); + + assert out.position() % getStride() == 0; + + //System.out.println("[" + vertexI + "] x: " + x + ", y: " + y + " z: " + z + ", u: " + u + ", v: " + v + ", b: " + b + ", c: " + c); + } + } + + public int quadCountByUVDirection(boolean v) { + if(v) { + return quadCountByDirection[uDirectionIs01 ? 0 : 1]; + } else { + return quadCountByDirection[uDirectionIs01 ? 1 : 0]; + } + } + + public static int getStride() { + return + 3 * 4 // XYZ (float) + + 2 * (/*Config.shortUV*/false ? 2 : 4) // UV (float) + + 4 // B (int) + + 4 // C (int) + ; + } + + private boolean isTranslatedCopyOf(Quad o, boolean checkValid) { + if((!isValid(this) && checkValid) || !isValid(o) || normal != o.normal) return false; + + for(int i = 1; i < 4; i++) { + double relX = xs[i] - xs[0]; + double relY = ys[i] - ys[0]; + double relZ = zs[i] - zs[0]; + + if(o.xs[i] != o.xs[0] + relX || o.ys[i] != o.ys[0] + relY || o.zs[i] != o.zs[0] + relZ) { + return false; + } + } + + for(int i = 0; i < 4; i++) { + if(us[i] != o.us[i] || vs[i] != o.vs[i] || bs[i] != o.bs[i] || cs[i] != o.cs[i]) { + return false; + } + } + + return true; + } + + private void copyVertexFrom(Quad o, int src, int dest) { + xs[dest] = o.xs[src]; + ys[dest] = o.ys[src]; + zs[dest] = o.zs[src]; + us[dest] = o.us[src]; + vs[dest] = o.vs[src]; + bs[dest] = o.bs[src]; + cs[dest] = o.cs[src]; + + updateMinMaxXYZ(); // TODO isn't doing this a waste? I should get rid of the min/maxXYZ variables entirely. + } + + private void updateMinMaxXYZ() { + for(int i = 0; i < 4; i++) { + minX = Math.min(minX, xs[i]); + minY = Math.min(minY, ys[i]); + minZ = Math.min(minZ, zs[i]); + maxX = Math.max(maxX, xs[i]); + maxY = Math.max(maxY, ys[i]); + maxZ = Math.max(maxZ, zs[i]); + } + } + + private void updateIsRectangle() { + isRectangle = + vertexExists(minX, minY, minZ) && + vertexExists(minX, minY, maxZ) && + vertexExists(minX, maxY, minZ) && + vertexExists(minX, maxY, maxZ) && + vertexExists(maxX, minY, minZ) && + vertexExists(maxX, minY, maxZ) && + vertexExists(maxX, maxY, minZ) && + vertexExists(maxX, maxY, maxZ); + } + + private boolean vertexExists(float x, float y, float z) { + for(int i = 0; i < 4; i++) { + if(xs[i] == x && ys[i] == y && zs[i] == z) { + return true; + } + } + return false; + } + + public static boolean isValid(Quad q) { + return q != null && !q.deleted; + } + + public boolean isClockwiseXZ() { + return (xs[1] - xs[0]) * (zs[2] - zs[0]) - (xs[2] - xs[0]) * (zs[1] - zs[0]) < 0; + } + + @Override + public String toString() { + return String.format(Locale.ENGLISH, "%s(%.1f, %.1f, %.1f -- %.1f, %.1f, %.1f)", deleted ? "XXX " : "", minX, minY, minZ, maxX, maxY, maxZ); + //return String.format(Locale.ENGLISH, "%s[(%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f)]", deleted ? "XXX " : "", xs[0], ys[0], zs[0], xs[1], ys[1], zs[1], xs[2], ys[2], zs[2], xs[3], ys[3], zs[3]); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/nd/RecyclingList.java b/src/main/java/com/gtnewhorizons/angelica/compat/nd/RecyclingList.java new file mode 100644 index 000000000..362cccc35 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/nd/RecyclingList.java @@ -0,0 +1,49 @@ +package com.gtnewhorizons.angelica.compat.nd; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class RecyclingList { + // Adapted from Neodymium + + private Supplier constructor; + + int nextIndex; + private final List list; + + public RecyclingList(Supplier constructor) { + this.constructor = constructor; + this.list = new ArrayList<>(); + } + + public T get(int i) { + while(list.size() <= i) { + list.add(constructor.get()); + } + return list.get(i); + } + + public T next() { + return get(nextIndex++); + } + + public void remove() { + if(nextIndex == 0) { + throw new IllegalStateException("Tried to remove from empty list"); + } + nextIndex--; + } + + public boolean isEmpty() { + return nextIndex == 0; + } + + public void reset() { + nextIndex = 0; + } + + public List getAsList() { + return list.subList(0, nextIndex); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DefaultVertexFormat.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DefaultVertexFormat.java new file mode 100644 index 000000000..d6a25f259 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DefaultVertexFormat.java @@ -0,0 +1,32 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.compat.mojang.VertexFormatElement; +import com.gtnewhorizons.angelica.compat.mojang.VertexFormatElement.Type; +import com.gtnewhorizons.angelica.compat.mojang.VertexFormatElement.Usage; + + +public class DefaultVertexFormat { + + public static final VertexFormatElement POSITION_ELEMENT = new VertexFormatElement(0, Type.FLOAT, Usage.POSITION, 3); + public static final VertexFormatElement COLOR_ELEMENT = new VertexFormatElement(0, Type.UBYTE, Usage.COLOR, 4); + public static final VertexFormatElement TEXTURE_0_ELEMENT = new VertexFormatElement(0, Type.FLOAT, Usage.UV, 2); + public static final VertexFormatElement OVERLAY_ELEMENT = new VertexFormatElement(1, Type.SHORT, Usage.UV, 2); + public static final VertexFormatElement LIGHT_ELEMENT = new VertexFormatElement(2, Type.SHORT, Usage.UV, 2); + public static final VertexFormatElement NORMAL_ELEMENT = new VertexFormatElement(0, Type.BYTE, Usage.NORMAL, 3); + public static final VertexFormatElement PADDING_ELEMENT = new VertexFormatElement(0, Type.BYTE, Usage.PADDING, 1); + public static final VertexFormat POSITION_COLOR_TEXTURE_LIGHT_NORMAL = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).add(TEXTURE_0_ELEMENT).add(LIGHT_ELEMENT).add(NORMAL_ELEMENT).add(PADDING_ELEMENT).build()); + public static final VertexFormat POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).add(TEXTURE_0_ELEMENT).add(OVERLAY_ELEMENT).add(LIGHT_ELEMENT).add(NORMAL_ELEMENT).add(PADDING_ELEMENT).build()); + public static final VertexFormat POSITION_TEXTURE_COLOR_LIGHT = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(TEXTURE_0_ELEMENT).add(COLOR_ELEMENT).add(LIGHT_ELEMENT).build()); + public static final VertexFormat POSITION = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).build()); + public static final VertexFormat POSITION_COLOR = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).build()); + public static final VertexFormat POSITION_COLOR_LIGHT = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).add(LIGHT_ELEMENT).build()); + public static final VertexFormat POSITION_TEXTURE = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(TEXTURE_0_ELEMENT).build()); + public static final VertexFormat POSITION_COLOR_TEXTURE = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).add(TEXTURE_0_ELEMENT).build()); + public static final VertexFormat POSITION_TEXTURE_COLOR = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(TEXTURE_0_ELEMENT).add(COLOR_ELEMENT).build()); + public static final VertexFormat POSITION_COLOR_TEX_LIGHTMAP = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(COLOR_ELEMENT).add(TEXTURE_0_ELEMENT).add(LIGHT_ELEMENT).build()); + public static final VertexFormat POSITION_TEXTURE_LIGHT_COLOR = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(TEXTURE_0_ELEMENT).add(LIGHT_ELEMENT).add(COLOR_ELEMENT).build()); + public static final VertexFormat POSITION_TEXTURE_COLOR_NORMAL = new VertexFormat(new ImmutableList.Builder().add(POSITION_ELEMENT).add(TEXTURE_0_ELEMENT).add(COLOR_ELEMENT).add(NORMAL_ELEMENT).add(PADDING_ELEMENT).build()); + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DrawState.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DrawState.java new file mode 100644 index 000000000..756e23e3e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/DrawState.java @@ -0,0 +1,20 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import lombok.Getter; + +@Deprecated +public class DrawState { + @Getter + private final VertexFormat format; + @Getter + private final int vertexCount; + @Getter + private final int mode; + + public DrawState(VertexFormat format, int vertexCount, int mode) { + this.format = format; + this.vertexCount = vertexCount; + this.mode = mode; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/MatrixStack.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/MatrixStack.java new file mode 100644 index 000000000..69632669e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/MatrixStack.java @@ -0,0 +1,110 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import com.google.common.collect.Queues; +import org.joml.Matrix3f; +import org.joml.Matrix4f; + +import java.util.Deque; + + +public class MatrixStack { + private final Deque matrixStack; + + public MatrixStack() { + this.matrixStack = Queues.newArrayDeque(); + matrixStack.add(new Entry(new Matrix4f().identity(), new Matrix3f().identity())); + } + + public MatrixStack(Matrix4f initial) { + this.matrixStack = Queues.newArrayDeque(); + Matrix3f normal = new Matrix3f(); + matrixStack.add(new Entry(initial, initial.normal(normal))); + } + + public Entry peek() { + return (Entry)this.matrixStack.getLast(); + } + + public void push() { + final Entry lv = (Entry)this.matrixStack.getLast(); + this.matrixStack.addLast(new Entry(new Matrix4f(lv.model), new Matrix3f(lv.normal))); + } + + public void pop() { + this.matrixStack.removeLast(); + } + public boolean clear() { + return this.matrixStack.size() == 1; + } + + public void translate(double d, double e, double f) { + final Entry lv = (Entry)this.matrixStack.getLast(); + lv.model.translate((float)d, (float)e, (float)f); + } + public void rotateX(float f) { + final Entry lv = (Entry)this.matrixStack.getLast(); + lv.model.rotateX(f); + lv.normal.rotateX(f); + } + + public void rotateY(float f) { + final Entry lv = (Entry)this.matrixStack.getLast(); + lv.model.rotateY(f); + lv.normal.rotateY(f); + } + + public void rotateZ(float f) { + final Entry lv = (Entry)this.matrixStack.getLast(); + lv.model.rotateZ(f); + lv.normal.rotateZ(f); + } + + + public void scale(float f, float g, float h) { + final Entry lv = (Entry)this.matrixStack.getLast(); + lv.model.scale(f, g, h); + + if (f == g && g == h) { + if (f > 0.0F) { + return; + } + + lv.normal.scale(-1.0F); + } + float i = 1.0F / f; + float j = 1.0F / g; + float k = 1.0F / h; + float l = invSqrt(i * j * k); + lv.normal.scale(l * i, l * j, l * k); + + } + + + private static float invSqrt(float x) { + float xhalf = 0.5f * x; + int i = Float.floatToIntBits(x); + i = 0x5f3759df - (i >> 1); + x = Float.intBitsToFloat(i); + x *= (1.5f - xhalf * x * x); + return x; + } + + public static final class Entry { + private final Matrix4f model; + private final Matrix3f normal; + + private Entry(Matrix4f model, Matrix3f normal) { + this.model = model; + this.normal = normal; + } + + public Matrix4f getModel() { + return model; + } + + public Matrix3f getNormal() { + return normal; + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/OverlayTexture.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/OverlayTexture.java new file mode 100644 index 000000000..54a3ab1f1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/OverlayTexture.java @@ -0,0 +1,8 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +@Deprecated +public class OverlayTexture { + + // This is definitely wrong + public static final int NO_OVERLAY = 0; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderLayer.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderLayer.java new file mode 100644 index 000000000..38045148e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderLayer.java @@ -0,0 +1,311 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; +import lombok.Getter; +import net.minecraft.util.ResourceLocation; + +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Optional; + +public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris) + private static final RenderLayer SOLID = of("solid", DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL, 7, 2097152, true, false, RenderLayer.MultiPhaseParameters.builder().shadeModel(SMOOTH_SHADE_MODEL).lightmap(ENABLE_LIGHTMAP).texture(MIPMAP_BLOCK_ATLAS_TEXTURE).build(true)); + private static final RenderLayer CUTOUT = of("cutout", DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL, 7, 131072, true, false, RenderLayer.MultiPhaseParameters.builder().shadeModel(SMOOTH_SHADE_MODEL).lightmap(ENABLE_LIGHTMAP).texture(BLOCK_ATLAS_TEXTURE).alpha(HALF_ALPHA).build(true)); + private static final RenderLayer TRANSLUCENT = of("translucent", DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL, 7, 262144, true, true, createTranslucentPhaseData()); + + @Getter + private final VertexFormat vertexFormat; + @Getter + private final int drawMode; + @Getter + private final int expectedBufferSize; + + public RenderLayer(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, Runnable startAction, Runnable endAction) { + super(name, startAction, endAction); + this.vertexFormat = vertexFormat; + this.drawMode = drawMode; + this.expectedBufferSize = expectedBufferSize; + } + + + public static MultiPhase of(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, MultiPhaseParameters phaseData) { + return of(name, vertexFormat, drawMode, expectedBufferSize, false, false, phaseData); + } + + public static MultiPhase of(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, boolean hasCrumbling, boolean translucent, MultiPhaseParameters phases) { + return RenderLayer.MultiPhase.of(name, vertexFormat, drawMode, expectedBufferSize, hasCrumbling, translucent, phases); + } + + + public static RenderLayer solid() { + return SOLID; + } + + public static RenderLayer cutout() { + return CUTOUT; + } + + private static MultiPhaseParameters createTranslucentPhaseData() { + return RenderLayer.MultiPhaseParameters.builder().shadeModel(SMOOTH_SHADE_MODEL).lightmap(ENABLE_LIGHTMAP).texture(MIPMAP_BLOCK_ATLAS_TEXTURE).transparency(TRANSLUCENT_TRANSPARENCY).target(TRANSLUCENT_TARGET).build(true); + } + + public static RenderLayer translucent() { + return TRANSLUCENT; + } + + public static RenderLayer getOutline(ResourceLocation texture, RenderPhase.Cull cull) { + return of("outline", DefaultVertexFormat.POSITION_COLOR_TEXTURE, 7, 256, RenderLayer.MultiPhaseParameters.builder().texture(new RenderPhase.Texture(texture, false, false)).cull(cull).depthTest(ALWAYS_DEPTH_TEST).alpha(ONE_TENTH_ALPHA).texturing(OUTLINE_TEXTURING).fog(NO_FOG).target(OUTLINE_TARGET).build(RenderLayer.OutlineMode.IS_OUTLINE)); + } + + public int mode() { + return this.drawMode; + } + + static final class MultiPhase extends RenderLayer { + private static final ObjectOpenCustomHashSet CACHE; + private final MultiPhaseParameters phases; + private final int hash; + private final Optional affectedOutline; + private final boolean outline; + + private MultiPhase(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, boolean hasCrumbling, boolean translucent, MultiPhaseParameters phases) { + super(name, vertexFormat, drawMode, expectedBufferSize, () -> { + phases.phases.forEach(RenderPhase::startDrawing); + }, () -> { + phases.phases.forEach(RenderPhase::endDrawing); + }); + this.phases = phases; + this.affectedOutline = phases.outlineMode == RenderLayer.OutlineMode.AFFECTS_OUTLINE ? phases.texture.getId().map((arg2) -> { + return getOutline(arg2, phases.cull); + }) : Optional.empty(); + this.outline = phases.outlineMode == RenderLayer.OutlineMode.IS_OUTLINE; + this.hash = Objects.hash(new Object[]{super.hashCode(), phases}); + } + + public static MultiPhase of(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, boolean hasCrumbling, boolean translucent, + MultiPhaseParameters phases) { + return (MultiPhase)CACHE.addOrGet(new MultiPhase(name, vertexFormat, drawMode, expectedBufferSize, hasCrumbling, translucent, phases)); + } + + public Optional getAffectedOutline() { + return this.affectedOutline; + } + + public boolean equals(@Nullable Object object) { + return this == object; + } + + public int hashCode() { + return this.hash; + } + + public String toString() { + return "RenderType[" + this.phases + ']'; + } + + static { + CACHE = new ObjectOpenCustomHashSet(RenderLayer.MultiPhase.HashStrategy.INSTANCE); + } + + static enum HashStrategy implements Hash.Strategy { + INSTANCE; + + private HashStrategy() { + } + + public int hashCode(@Nullable MultiPhase arg) { + return arg == null ? 0 : arg.hash; + } + + public boolean equals(@Nullable MultiPhase arg, @Nullable MultiPhase arg2) { + if (arg == arg2) { + return true; + } else { + return arg != null && arg2 != null ? Objects.equals(arg.phases, arg2.phases) : false; + } + } + } + } + + public static final class MultiPhaseParameters { + private final RenderPhase.Texture texture; + private final RenderPhase.Transparency transparency; + private final RenderPhase.DiffuseLighting diffuseLighting; + private final RenderPhase.ShadeModel shadeModel; + private final RenderPhase.Alpha alpha; + private final RenderPhase.DepthTest depthTest; + private final RenderPhase.Cull cull; + private final RenderPhase.Lightmap lightmap; + private final RenderPhase.Overlay overlay; + private final RenderPhase.Fog fog; + private final RenderPhase.Layering layering; + private final RenderPhase.Target target; + private final RenderPhase.Texturing texturing; + private final RenderPhase.WriteMaskState writeMaskState; + private final RenderPhase.LineWidth lineWidth; + private final OutlineMode outlineMode; + private final ImmutableList phases; + + private MultiPhaseParameters(RenderPhase.Texture texture, RenderPhase.Transparency transparency, RenderPhase.DiffuseLighting diffuseLighting, RenderPhase.ShadeModel shadeModel, RenderPhase.Alpha alpha, RenderPhase.DepthTest depthTest, RenderPhase.Cull cull, RenderPhase.Lightmap lightmap, RenderPhase.Overlay overlay, RenderPhase.Fog fog, RenderPhase.Layering layering, RenderPhase.Target target, RenderPhase.Texturing texturing, RenderPhase.WriteMaskState writeMaskState, RenderPhase.LineWidth lineWidth, OutlineMode outlineMode) { + this.texture = texture; + this.transparency = transparency; + this.diffuseLighting = diffuseLighting; + this.shadeModel = shadeModel; + this.alpha = alpha; + this.depthTest = depthTest; + this.cull = cull; + this.lightmap = lightmap; + this.overlay = overlay; + this.fog = fog; + this.layering = layering; + this.target = target; + this.texturing = texturing; + this.writeMaskState = writeMaskState; + this.lineWidth = lineWidth; + this.outlineMode = outlineMode; + this.phases = ImmutableList.of(this.texture, this.transparency, this.diffuseLighting, this.shadeModel, this.alpha, this.depthTest, this.cull, this.lightmap, this.overlay, this.fog, this.layering, this.target, new RenderPhase[]{this.texturing, this.writeMaskState, this.lineWidth}); + } + + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + MultiPhaseParameters rendertype$state = (MultiPhaseParameters)object; + return this.outlineMode == rendertype$state.outlineMode && this.phases.equals(rendertype$state.phases); + } else { + return false; + } + } + + public int hashCode() { + return Objects.hash(new Object[]{this.phases, this.outlineMode}); + } + + public String toString() { + return "CompositeState[" + this.phases + ", outlineProperty=" + this.outlineMode + ']'; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RenderPhase.Texture texture; + private RenderPhase.Transparency transparency; + private RenderPhase.DiffuseLighting diffuseLighting; + private RenderPhase.ShadeModel shadeModel; + private RenderPhase.Alpha alpha; + private RenderPhase.DepthTest depthTest; + private RenderPhase.Cull cull; + private RenderPhase.Lightmap lightmap; + private RenderPhase.Overlay overlay; + private RenderPhase.Fog fog; + private RenderPhase.Layering layering; + private RenderPhase.Target target; + private RenderPhase.Texturing texturing; + private RenderPhase.WriteMaskState writeMaskState; + private RenderPhase.LineWidth lineWidth; + + private Builder() { + this.texture = RenderPhase.NO_TEXTURE ; + this.transparency = RenderPhase.NO_TRANSPARENCY; + this.diffuseLighting = RenderPhase.DISABLE_DIFFUSE_LIGHTING; + this.shadeModel = RenderPhase.SHADE_MODEL; + this.alpha = RenderPhase.ZERO_ALPHA; + this.depthTest = RenderPhase.LEQUAL_DEPTH_TEST; + this.cull = RenderPhase.ENABLE_CULLING; + this.lightmap = RenderPhase.DISABLE_LIGHTMAP; + this.overlay = RenderPhase.DISABLE_OVERLAY_COLOR; + this.fog = RenderPhase.FOG; + this.layering = RenderPhase.NO_LAYERING; + this.target = RenderPhase.MAIN_TARGET; + this.texturing = RenderPhase.DEFAULT_TEXTURING; + this.writeMaskState = RenderPhase.ALL_MASK; + this.lineWidth = RenderPhase.FULL_LINE_WIDTH; + } + + public Builder texture(RenderPhase.Texture texture) { + this.texture = texture; + return this; + } + + public Builder transparency(RenderPhase.Transparency transparency) { + this.transparency = transparency; + return this; + } + + public Builder shadeModel(RenderPhase.ShadeModel shadeModel) { + this.shadeModel = shadeModel; + return this; + } + + public Builder alpha(RenderPhase.Alpha alpha) { + this.alpha = alpha; + return this; + } + + public Builder depthTest(RenderPhase.DepthTest depthTest) { + this.depthTest = depthTest; + return this; + } + + public Builder cull(RenderPhase.Cull cull) { + this.cull = cull; + return this; + } + + public Builder lightmap(RenderPhase.Lightmap lightmap) { + this.lightmap = lightmap; + return this; + } + + public Builder overlay(RenderPhase.Overlay overlay) { + this.overlay = overlay; + return this; + } + + public Builder fog(RenderPhase.Fog fog) { + this.fog = fog; + return this; + } + + public Builder target(RenderPhase.Target target) { + this.target = target; + return this; + } + + public Builder texturing(RenderPhase.Texturing texturing) { + this.texturing = texturing; + return this; + } + + public MultiPhaseParameters build(boolean affectsOutline) { + return this.build(affectsOutline ? RenderLayer.OutlineMode.AFFECTS_OUTLINE : RenderLayer.OutlineMode.NONE); + } + + public MultiPhaseParameters build(OutlineMode outlineMode) { + return new MultiPhaseParameters(this.texture, this.transparency, this.diffuseLighting, this.shadeModel, this.alpha, this.depthTest, this.cull, this.lightmap, this.overlay, this.fog, this.layering, this.target, this.texturing, this.writeMaskState, this.lineWidth, outlineMode); + } + } + } + + enum OutlineMode { + NONE("none"), + IS_OUTLINE("is_outline"), + AFFECTS_OUTLINE("affects_outline"); + + private final String name; + + OutlineMode(String name) { + this.name = name; + } + + public String toString() { + return this.name; + } + } + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderPhase.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderPhase.java new file mode 100644 index 000000000..6ba033e3b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/RenderPhase.java @@ -0,0 +1,816 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; + + +public abstract class RenderPhase { + protected final String name; + protected Runnable beginAction; + private final Runnable endAction; + protected static final Transparency NO_TRANSPARENCY = new Transparency("no_transparency", GLStateManager::disableBlend, () -> { + }); + protected static final Transparency ADDITIVE_TRANSPARENCY = new Transparency("additive_transparency", () -> { + GLStateManager.enableBlend(); + GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); + }, () -> { + GLStateManager.disableBlend(); + GLStateManager.defaultBlendFunc(); + }); + protected static final Transparency LIGHTNING_TRANSPARENCY = new Transparency("lightning_transparency", () -> { + GLStateManager.enableBlend(); + GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE); + }, () -> { + GLStateManager.disableBlend(); + GLStateManager.defaultBlendFunc(); + }); + protected static final Transparency GLINT_TRANSPARENCY = new Transparency("glint_transparency", () -> { + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_COLOR, GL11.GL_ONE, GL11.GL_ZERO, GL11.GL_ONE); + }, () -> { + GLStateManager.disableBlend(); + GLStateManager.defaultBlendFunc(); + }); + protected static final Transparency CRUMBLING_TRANSPARENCY = new Transparency("crumbling_transparency", () -> { + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_SRC_COLOR, GL11.GL_ONE, GL11.GL_ZERO); + }, () -> { + GLStateManager.disableBlend(); + GLStateManager.defaultBlendFunc(); + }); + protected static final Transparency TRANSLUCENT_TRANSPARENCY = new Transparency("translucent_transparency", () -> { + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + }, () -> { + GLStateManager.disableBlend(); + GLStateManager.defaultBlendFunc(); + }); + protected static final Alpha ZERO_ALPHA = new Alpha(0.0F); + protected static final Alpha ONE_TENTH_ALPHA = new Alpha(0.003921569F); + protected static final Alpha HALF_ALPHA = new Alpha(0.5F); + protected static final ShadeModel SHADE_MODEL = new ShadeModel(false); + protected static final ShadeModel SMOOTH_SHADE_MODEL = new ShadeModel(true); + protected static final Texture MIPMAP_BLOCK_ATLAS_TEXTURE; + protected static final Texture BLOCK_ATLAS_TEXTURE; + protected static final Texture NO_TEXTURE; + protected static final Texturing DEFAULT_TEXTURING; + protected static final Texturing OUTLINE_TEXTURING; + protected static final Texturing GLINT_TEXTURING; + protected static final Texturing ENTITY_GLINT_TEXTURING; + protected static final Lightmap ENABLE_LIGHTMAP; + protected static final Lightmap DISABLE_LIGHTMAP; + protected static final Overlay ENABLE_OVERLAY_COLOR; + protected static final Overlay DISABLE_OVERLAY_COLOR; + protected static final DiffuseLighting ENABLE_DIFFUSE_LIGHTING; + protected static final DiffuseLighting DISABLE_DIFFUSE_LIGHTING; + protected static final Cull ENABLE_CULLING; + protected static final Cull DISABLE_CULLING; + protected static final DepthTest ALWAYS_DEPTH_TEST; + protected static final DepthTest EQUAL_DEPTH_TEST; + protected static final DepthTest LEQUAL_DEPTH_TEST; + protected static final WriteMaskState ALL_MASK; + protected static final WriteMaskState COLOR_MASK; + protected static final WriteMaskState DEPTH_MASK; + protected static final Layering NO_LAYERING; + protected static final Layering POLYGON_OFFSET_LAYERING; + protected static final Layering VIEW_OFFSET_Z_LAYERING; + protected static final Fog NO_FOG; + protected static final Fog FOG; + protected static final Fog BLACK_FOG; + protected static final Target MAIN_TARGET; + protected static final Target OUTLINE_TARGET; + protected static final Target TRANSLUCENT_TARGET; + protected static final Target PARTICLES_TARGET; + protected static final Target WEATHER_TARGET; + protected static final Target CLOUDS_TARGET; + protected static final Target ITEM_TARGET; + protected static final LineWidth FULL_LINE_WIDTH; + + public RenderPhase(String name, Runnable beginAction, Runnable endAction) { + this.name = name; + this.beginAction = beginAction; + this.endAction = endAction; + } + + public void startDrawing() { + this.beginAction.run(); + } + + public void endDrawing() { + this.endAction.run(); + } + + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + RenderPhase lv = (RenderPhase)object; + return this.name.equals(lv.name); + } else { + return false; + } + } + + public int hashCode() { + return this.name.hashCode(); + } + + public String toString() { + return this.name; + } + + private static void setupGlintTexturing(float scale) { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.pushMatrix(); +// RenderSystem.loadIdentity(); +// long l = Util.getMeasuringTimeMs() * 8L; +// float g = (float)(l % 110000L) / 110000.0F; +// float h = (float)(l % 30000L) / 30000.0F; +// RenderSystem.translatef(-g, h, 0.0F); +// RenderSystem.rotatef(10.0F, 0.0F, 0.0F, 1.0F); +// RenderSystem.scalef(scale, scale, scale); +// RenderSystem.matrixMode(5888); + } + + private static ResourceLocation ATLAS = TextureMap.locationBlocksTexture; + static { + // TODO: Sodium - SpriteAtlasTexture + MIPMAP_BLOCK_ATLAS_TEXTURE = new Texture(ATLAS, false, true); + BLOCK_ATLAS_TEXTURE = new Texture(ATLAS, false, false); + NO_TEXTURE = new Texture(); + DEFAULT_TEXTURING = new Texturing("default_texturing", () -> { + }, () -> { + }); + OUTLINE_TEXTURING = new Texturing("outline_texturing", () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.setupOutline(); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.teardownOutline(); + }); + GLINT_TEXTURING = new Texturing("glint_texturing", () -> { + setupGlintTexturing(8.0F); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.popMatrix(); +// RenderSystem.matrixMode(5888); + }); + ENTITY_GLINT_TEXTURING = new Texturing("entity_glint_texturing", () -> { + setupGlintTexturing(0.16F); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.popMatrix(); +// RenderSystem.matrixMode(5888); + }); + ENABLE_LIGHTMAP = new Lightmap(true); + DISABLE_LIGHTMAP = new Lightmap(false); + ENABLE_OVERLAY_COLOR = new Overlay(true); + DISABLE_OVERLAY_COLOR = new Overlay(false); + ENABLE_DIFFUSE_LIGHTING = new DiffuseLighting(true); + DISABLE_DIFFUSE_LIGHTING = new DiffuseLighting(false); + ENABLE_CULLING = new Cull(true); + DISABLE_CULLING = new Cull(false); + ALWAYS_DEPTH_TEST = new DepthTest("always", GL11.GL_ALWAYS); + EQUAL_DEPTH_TEST = new DepthTest("==", GL11.GL_EQUAL); + LEQUAL_DEPTH_TEST = new DepthTest("<=", GL11.GL_LEQUAL); + ALL_MASK = new WriteMaskState(true, true); + COLOR_MASK = new WriteMaskState(true, false); + DEPTH_MASK = new WriteMaskState(false, true); + NO_LAYERING = new Layering("no_layering", () -> { + }, () -> { + }); + POLYGON_OFFSET_LAYERING = new Layering("polygon_offset_layering", () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.polygonOffset(-1.0F, -10.0F); +// RenderSystem.enablePolygonOffset(); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.polygonOffset(0.0F, 0.0F); +// RenderSystem.disablePolygonOffset(); + }); + VIEW_OFFSET_Z_LAYERING = new Layering("view_offset_z_layering", () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.pushMatrix(); +// RenderSystem.scalef(0.99975586F, 0.99975586F, 0.99975586F); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem::popMatrix(); + }); + NO_FOG = new Fog("no_fog", () -> { + }, () -> { + }); + FOG = new Fog("fog", () -> { + // Unclear what this should do + // BackgroundRenderer.setFogBlack(), also levelFogColor() + SodiumClientMod.LOGGER.debug("Fog - Not setting level fog color"); + GLStateManager.enableFog(); + }, GLStateManager::disableFog); + BLACK_FOG = new Fog("black_fog", () -> { + GLStateManager.fogColor(0.0F, 0.0F, 0.0F, 1.0F); + GLStateManager.enableFog(); + }, () -> { + // Unclear what this should do + // BackgroundRenderer.setFogBlack(), also levelFogColor() + SodiumClientMod.LOGGER.debug("Fog - Not setting level fog color"); + GLStateManager.disableFog(); + }); + MAIN_TARGET = new Target("main_target", () -> { + }, () -> { + }); + OUTLINE_TARGET = new Target("outline_target", () -> { + // TODO: Sodium + SodiumClientMod.LOGGER.debug("NOT enabling the entity outline framebuffer"); + //MinecraftClient.getInstance().worldRenderer.getEntityOutlinesFramebuffer().beginWrite(false); + }, () -> { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + }); + TRANSLUCENT_TARGET = new Target("translucent_target", () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + // TODO: Sodium + SodiumClientMod.LOGGER.debug("NOT enabling the translucent framebuffer"); + // MinecraftClient.getInstance().worldRenderer.getTranslucentFramebuffer().beginWrite(false); + } + + }, () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + } + + }); + PARTICLES_TARGET = new Target("particles_target", () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().worldRenderer.getParticlesFramebuffer().beginWrite(false); + } + + }, () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + } + + }); + WEATHER_TARGET = new Target("weather_target", () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().worldRenderer.getWeatherFramebuffer().beginWrite(false); + } + + }, () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + } + + }); + CLOUDS_TARGET = new Target("clouds_target", () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().worldRenderer.getCloudsFramebuffer().beginWrite(false); + } + + }, () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + } + + }); + ITEM_TARGET = new Target("item_entity_target", () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().worldRenderer.getEntityFramebuffer().beginWrite(false); + } + + }, () -> { + if (Minecraft.isFancyGraphicsEnabled()) { + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + } + + }); + FULL_LINE_WIDTH = new LineWidth(OptionalDouble.of(1.0)); + } + + + public static class LineWidth extends RenderPhase { + private final OptionalDouble width; + + public LineWidth(OptionalDouble optionalDouble) { + super("line_width", () -> { + if (!Objects.equals(optionalDouble, OptionalDouble.of(1.0))) { + if (optionalDouble.isPresent()) { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.lineWidth((float)optionalDouble.getAsDouble()); + } else { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.lineWidth(Math.max(2.5F, (float)MinecraftClient.getInstance().getWindow().getFramebufferWidth() / 1920.0F * 2.5F)); + } + } + + }, () -> { + if (!Objects.equals(optionalDouble, OptionalDouble.of(1.0))) { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.lineWidth(1.0F); + } + + }); + this.width = optionalDouble; + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + return !super.equals(object) ? false : Objects.equals(this.width, ((LineWidth)object).width); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.width); + } + + @Override + public String toString() { + return this.name + '[' + (this.width.isPresent() ? this.width.getAsDouble() : "window_scale") + ']'; + } + } + + + public static class Target extends RenderPhase { + public Target(String string, Runnable runnable, Runnable runnable2) { + super(string, runnable, runnable2); + } + } + + + public static class Fog extends RenderPhase { + public Fog(String string, Runnable runnable, Runnable runnable2) { + super(string, runnable, runnable2); + } + } + + + public static class Layering extends RenderPhase { + public Layering(String string, Runnable runnable, Runnable runnable2) { + super(string, runnable, runnable2); + } + } + + + public static class WriteMaskState extends RenderPhase { + private final boolean color; + private final boolean depth; + + public WriteMaskState(boolean color, boolean depth) { + super("write_mask_state", () -> { + if (!depth) { + GLStateManager.glDepthMask(depth); + } + + if (!color) { + GLStateManager.glColorMask(color, color, color, color); + } + + }, () -> { + if (!depth) { + GLStateManager.glDepthMask(true); + } + + if (!color) { + GLStateManager.glColorMask(true, true, true, true); + } + + }); + this.color = color; + this.depth = depth; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + WriteMaskState lv = (WriteMaskState)object; + return this.color == lv.color && this.depth == lv.depth; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(this.color, this.depth); + } + + @Override + public String toString() { + return this.name + "[writeColor=" + this.color + ", writeDepth=" + this.depth + ']'; + } + } + + + public static class DepthTest extends RenderPhase { + private final String depthFunction; + private final int func; + + public DepthTest(String string, int i) { + super("depth_test", () -> { + if (i != GL11.GL_ALWAYS) { + GLStateManager.enableDepthTest(); + GLStateManager.glDepthFunc(i); + } + + }, () -> { + if (i != GL11.GL_ALWAYS) { + GLStateManager.disableDepthTest(); + GLStateManager.glDepthFunc(GL11.GL_LEQUAL); + } + + }); + this.depthFunction = string; + this.func = i; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + DepthTest lv = (DepthTest)object; + return this.func == lv.func; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Integer.hashCode(this.func); + } + + @Override + public String toString() { + return this.name + '[' + this.depthFunction + ']'; + } + } + + + public static class Cull extends Toggleable { + public Cull(boolean culling) { + super("cull", () -> { + if (!culling) { + GLStateManager.disableCull(); + } + + }, () -> { + if (!culling) { + GLStateManager.enableCull(); + } + + }, culling); + } + } + + + public static class DiffuseLighting extends Toggleable { + public DiffuseLighting(boolean guiLighting) { + super("diffuse_lighting", () -> { + if (guiLighting) { + throw new RuntimeException("Not Implemented Yet"); +// net.minecraft.client.drawScreen.DiffuseLighting.enable(); + } + + }, () -> { + if (guiLighting) { + throw new RuntimeException("Not Implemented Yet"); +// net.minecraft.client.drawScreen.DiffuseLighting.disable(); + } + + }, guiLighting); + } + } + + + public static class Overlay extends Toggleable { + public Overlay(boolean overlayColor) { + super("overlay", () -> { + if (overlayColor) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().gameRenderer.getOverlayTexture().setupOverlayColor(); + } + + }, () -> { + if (overlayColor) { + throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().gameRenderer.getOverlayTexture().teardownOverlayColor(); + } + + }, overlayColor); + } + } + + + public static class Lightmap extends Toggleable { + public Lightmap(boolean lightmap) { + super("lightmap", () -> { + if (lightmap) { + // TODO: Sodium - LightmapTextureManager + SodiumClientMod.LOGGER.debug("Lightmap - enable (not implemented)"); +// throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().enable(); + + } + + }, () -> { + if (lightmap) { + // TODO: Sodium - LightmapTextureManager + SodiumClientMod.LOGGER.debug("Lightmap - disable (not implemented)"); +// throw new RuntimeException("Not Implemented Yet"); +// MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().disable(); + } + + }, lightmap); + } + } + + + static class Toggleable extends RenderPhase { + private final boolean enabled; + + public Toggleable(String string, Runnable runnable, Runnable runnable2, boolean bl) { + super(string, runnable, runnable2); + this.enabled = bl; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + Toggleable lv = (Toggleable)object; + return this.enabled == lv.enabled; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Boolean.hashCode(this.enabled); + } + + @Override + public String toString() { + return this.name + '[' + this.enabled + ']'; + } + } + + + public static final class PortalTexturing extends Texturing { + private final int layer; + + public PortalTexturing(int layer) { + super("portal_texturing", () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.pushMatrix(); +// RenderSystem.loadIdentity(); +// RenderSystem.translatef(0.5F, 0.5F, 0.0F); +// RenderSystem.scalef(0.5F, 0.5F, 1.0F); +// RenderSystem.translatef(17.0F / (float)layer, (2.0F + (float)layer / 1.5F) * ((float)(Util.getMeasuringTimeMs() % 800000L) / 800000.0F), 0.0F); +// RenderSystem.rotatef(((float)(layer * layer) * 4321.0F + (float)layer * 9.0F) * 2.0F, 0.0F, 0.0F, 1.0F); +// RenderSystem.scalef(4.5F - (float)layer / 4.0F, 4.5F - (float)layer / 4.0F, 1.0F); +// RenderSystem.mulTextureByProjModelView(); +// RenderSystem.matrixMode(5888); +// RenderSystem.setupEndPortalTexGen(); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.popMatrix(); +// RenderSystem.matrixMode(5888); +// RenderSystem.clearTexGen(); + }); + this.layer = layer; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + PortalTexturing lv = (PortalTexturing)object; + return this.layer == lv.layer; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Integer.hashCode(this.layer); + } + } + + + public static final class OffsetTexturing extends Texturing { + private final float x; + private final float y; + + public OffsetTexturing(float x, float y) { + super("offset_texturing", () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.pushMatrix(); +// RenderSystem.loadIdentity(); +// RenderSystem.translatef(x, y, 0.0F); +// RenderSystem.matrixMode(5888); + }, () -> { + throw new RuntimeException("Not Implemented Yet"); +// RenderSystem.matrixMode(5890); +// RenderSystem.popMatrix(); +// RenderSystem.matrixMode(5888); + }); + this.x = x; + this.y = y; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + OffsetTexturing lv = (OffsetTexturing)object; + return Float.compare(lv.x, this.x) == 0 && Float.compare(lv.y, this.y) == 0; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(this.x, this.y); + } + } + + + public static class Texturing extends RenderPhase { + public Texturing(String string, Runnable runnable, Runnable runnable2) { + super(string, runnable, runnable2); + } + } + + + public static class Texture extends RenderPhase { + private final Optional id; + protected boolean bilinear; + protected boolean mipmap; + + public Texture(ResourceLocation id, boolean bilinear, boolean mipmap) { + super("texture", () -> { + GLStateManager.enableTexture(); + TextureManager lv = Minecraft.getMinecraft().getTextureManager(); + lv.bindTexture(id); + //GLStateManager.setFilter(bilinear, mipmap); // breaks textures. TODO find out why + }, () -> { + }); + this.id = Optional.of(id); + this.bilinear = bilinear; + this.mipmap = mipmap; + } + + public Texture() { + super("texture", GLStateManager::disableTexture, GLStateManager::enableTexture); + this.id = Optional.empty(); + this.bilinear = false; + this.mipmap = false; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + Texture lv = (Texture)object; + return this.id.equals(lv.id) && this.bilinear == lv.bilinear && this.mipmap == lv.mipmap; + } else { + return false; + } + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public String toString() { + return this.name + '[' + this.id + "(blur=" + this.bilinear + ", mipmap=" + this.mipmap + ")]"; + } + + protected Optional getId() { + return this.id; + } + } + + + public static class ShadeModel extends RenderPhase { + private final boolean smooth; + + public ShadeModel(boolean smooth) { + super("shade_model", + () -> GLStateManager.glShadeModel(smooth ? GL11.GL_SMOOTH : GL11.GL_FLAT), + () -> GLStateManager.glShadeModel(GL11.GL_FLAT)); + this.smooth = smooth; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + ShadeModel lv = (ShadeModel)object; + return this.smooth == lv.smooth; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Boolean.hashCode(this.smooth); + } + + @Override + public String toString() { + return this.name + '[' + (this.smooth ? "smooth" : "flat") + ']'; + } + } + + + public static class Alpha extends RenderPhase { + private final float alpha; + + public Alpha(float alpha) { + super("alpha", () -> { + if (alpha > 0.0F) { + + GLStateManager.enableAlphaTest(); + GLStateManager.glAlphaFunc(GL11.GL_GREATER, alpha); + } else { + GLStateManager.disableAlphaTest(); + } + + }, () -> { + GLStateManager.disableAlphaTest(); + GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1F); + }); + this.alpha = alpha; + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (object != null && this.getClass() == object.getClass()) { + if (!super.equals(object)) { + return false; + } else { + return this.alpha == ((Alpha)object).alpha; + } + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.alpha); + } + + @Override + public String toString() { + return this.name + '[' + this.alpha + ']'; + } + } + + + public static class Transparency extends RenderPhase { + public Transparency(String string, Runnable runnable, Runnable runnable2) { + super(string, runnable, runnable2); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexConsumer.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexConsumer.java new file mode 100644 index 000000000..e60530185 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexConsumer.java @@ -0,0 +1,48 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import javax.annotation.Nonnull; + +public interface VertexConsumer { + + VertexConsumer vertex(double d, double e, double f); + + @Nonnull + VertexConsumer color(int r, int g, int b, int a); + + @Nonnull + VertexConsumer texture(float u, float v); + + @Nonnull + VertexConsumer overlay(int u, int v); + + @Nonnull + VertexConsumer light(int u, int v); + + @Nonnull + VertexConsumer normal(float x, float y, float z); + + void next(); + + default VertexConsumer overlay(int overlay) { + return this.overlay(overlay & 0xFFFF, overlay >> 16 & 0xFFFF); + } + + default VertexConsumer light(int light) { + return this.light(light & 0xFFFF, light >> 16 & 0xFFFF); + } + + default VertexConsumer color(float red, float green, float blue, float alpha) { + return this.color((int)(red * 255.0F), (int)(green * 255.0F), (int)(blue * 255.0F), (int)(alpha * 255.0F)); + } + + default void vertex(float x, float y, float z, float red, float green, float blue, float alpha, float u, float v, int overlay, int light, float normalX, float normalY, float normalZ) { + this.vertex((double)x, (double)y, (double)z); + this.color(red, green, blue, alpha); + this.texture(u, v); + this.overlay(overlay); + this.light(light); + this.normal(normalX, normalY, normalZ); + this.next(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexFormat.java b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexFormat.java new file mode 100644 index 000000000..5efd8b511 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/toremove/VertexFormat.java @@ -0,0 +1,67 @@ +package com.gtnewhorizons.angelica.compat.toremove; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.compat.mojang.VertexFormatElement; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.Getter; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.vertices.ImmediateState; +import net.coderbot.iris.vertices.IrisVertexFormats; + +import java.util.List; + +public class VertexFormat { + @Getter + protected final ImmutableList elements; + protected final IntList offsets = new IntArrayList(); + @Getter + protected final int vertexSize; + + public VertexFormat(ImmutableList elements) { + this.elements = elements; + int i = 0; + for (VertexFormatElement element : elements) { + offsets.add(i); + i += element.getByteSize(); + } + vertexSize = i; + } + + @Deprecated + public void setupBufferState(long l) { + if (BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat() && ImmediateState.renderWithExtendedVertexFormat) { + if (this == DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL || this == DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP) { + IrisVertexFormats.TERRAIN.setupBufferState(l); + return; + } else if (this == DefaultVertexFormat.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL) { + IrisVertexFormats.ENTITY.setupBufferState(l); + return; + } + } + + final int i = this.getVertexSize(); + final List list = this.getElements(); + + for(int j = 0; j < list.size(); ++j) { + ((VertexFormatElement)list.get(j)).setupBufferState(l + (long)this.offsets.getInt(j), i); + } + } + + @Deprecated + public void clearBufferState() { + if (BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat() && ImmediateState.renderWithExtendedVertexFormat) { + if (this == DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL || this == DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP) { + IrisVertexFormats.TERRAIN.clearBufferState(); + return; + } else if ( this == DefaultVertexFormat.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL) { + IrisVertexFormats.ENTITY.clearBufferState(); + return; + } + } + for (VertexFormatElement vertexformatelement : this.getElements()) { + vertexformatelement.clearBufferState(); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java new file mode 100644 index 000000000..cc110d193 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java @@ -0,0 +1,71 @@ +package com.gtnewhorizons.angelica.config; + +@Config(modid = "angelica") +public class AngelicaConfig { + @Config.Comment("Enable Sodium rendering") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean enableSodium; + + @Config.Comment("Enable Sodium fluid rendering") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean enableSodiumFluidRendering; + + @Config.Comment("Enable Iris Shaders [Requires Sodium]") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean enableIris; + + @Config.Comment("Enable NotFine optimizations") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean enableNotFineOptimizations; + + @Config.Comment("Enable NotFine features") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean enableNotFineFeatures; + + @Config.Comment("Tweak F3 screen to be closer to modern versions. [From ArchaicFix]") + @Config.DefaultBoolean(true) + public static boolean modernizeF3Screen; + + @Config.Comment("Show block registry name and meta value in F3, similar to 1.8+. [From ArchaicFix]") + @Config.DefaultBoolean(true) + public static boolean showBlockDebugInfo; + + @Config.DefaultBoolean(true) + @Config.Comment("Hide downloading terrain screen. [From ArchaicFix]") + public static boolean hideDownloadingTerrainScreen; + + @Config.Comment("Show memory usage during game load. [From ArchaicFix]") + @Config.DefaultBoolean(true) + public static boolean showSplashMemoryBar; + + + @Config.Comment("Renders the HUD elements once per tick and reuses the pixels to improve performance. [Experimental]") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean enableHudCaching; + + @Config.Comment("Batch drawScreen fonts [Experimental]") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean enableFontRenderer; + + @Config.Comment("Optimize world update light. [From Hodgepodge]") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean optimizeWorldUpdateLight; + + @Config.Comment("Speedup Animations. [From Hodgepodge]") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean speedupAnimations; + + @Config.Comment("Optimize Texture Loading. [From Hodgepodge]") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean optimizeTextureLoading; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfig.java b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfig.java new file mode 100644 index 000000000..09377919c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfig.java @@ -0,0 +1,9 @@ +package com.gtnewhorizons.angelica.config; + +import net.minecraft.client.gui.GuiScreen; + +public class AngelicaGuiConfig extends SimpleGuiConfig { + public AngelicaGuiConfig(GuiScreen parent) throws ConfigException { + super(parent, AngelicaConfig.class, "archaicfix", "ArchaicFix"); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfigFactory.java b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfigFactory.java new file mode 100644 index 000000000..73080f10c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaGuiConfigFactory.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.config; + +import net.minecraft.client.gui.GuiScreen; + +public class AngelicaGuiConfigFactory implements SimpleGuiFactory { + @Override + public Class mainConfigGuiClass() { + return AngelicaGuiConfig.class; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/Config.java b/src/main/java/com/gtnewhorizons/angelica/config/Config.java new file mode 100644 index 000000000..647d2e9c7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/Config.java @@ -0,0 +1,113 @@ +package com.gtnewhorizons.angelica.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Config { + /** + * The mod id that this configuration is associated with. + */ + String modid(); + + /** + * Root element category, defaults to "general". You must not specify an empty string. + */ + String category() default "general"; + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.TYPE}) + @interface LangKey { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Comment { + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Ignore { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultBoolean { + boolean value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface RangeInt { + int min() default Integer.MIN_VALUE; + + int max() default Integer.MAX_VALUE; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultInt { + int value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface RangeFloat { + float min() default Float.MIN_VALUE; + + float max() default Float.MAX_VALUE; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultFloat { + float value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultString { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Pattern { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultEnum { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface DefaultStringList { + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Name { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.TYPE}) + @interface RequiresMcRestart { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.TYPE}) + @interface RequiresWorldRestart { + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/ConfigException.java b/src/main/java/com/gtnewhorizons/angelica/config/ConfigException.java new file mode 100644 index 000000000..2be81c802 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/ConfigException.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.angelica.config; + +/** + * A really basic wrapper for config to simplify handling them in external code. + */ +public class ConfigException extends Exception { + + public ConfigException(String message) { + super(message); + } + + public ConfigException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/ConfigurationManager.java b/src/main/java/com/gtnewhorizons/angelica/config/ConfigurationManager.java new file mode 100644 index 000000000..7681bb418 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/ConfigurationManager.java @@ -0,0 +1,267 @@ +package com.gtnewhorizons.angelica.config; + +import cpw.mods.fml.client.config.IConfigElement; +import cpw.mods.fml.client.event.ConfigChangedEvent; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.val; +import net.minecraft.launchwrapper.Launch; +import net.minecraftforge.common.config.ConfigElement; +import net.minecraftforge.common.config.Configuration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Class for controlling the loading of configuration files. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConfigurationManager { + private static final Logger LOGGER = LogManager.getLogger("ArchaicConfig"); + private static final Map configs = new HashMap<>(); + + private static final Map>> configToClassMap = new HashMap<>(); + + private static final ConfigurationManager instance = new ConfigurationManager(); + + private static boolean initialized = false; + + private static Path configDir; + + /** + * Registers a configuration class to be loaded. This should be done in preInit. + * + * @param configClass The class to register. + */ + public static void registerConfig(Class configClass) throws ConfigException { + init(); + val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .orElseThrow(() -> new ConfigException( + "Class " + configClass.getName() + " does not have a @Config annotation!")); + val category = Optional.of(cfg.category().trim()) + .map((cat) -> cat.length() == 0 ? null : cat) + .orElseThrow(() -> new ConfigException( + "Config class " + configClass.getName() + " has an empty category!")); + val rawConfig = configs.computeIfAbsent(cfg.modid(), (ignored) -> { + val c = new Configuration(configDir.resolve(cfg.modid() + ".cfg").toFile()); + c.load(); + return c; + }); + configToClassMap.computeIfAbsent(rawConfig, (ignored) -> new HashSet<>()).add(configClass); + try { + processConfigInternal(configClass, category, rawConfig); + rawConfig.save(); + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException e) { + throw new ConfigException(e); + } + } + + private static void processConfigInternal(Class configClass, String category, Configuration rawConfig) + throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, + ConfigException { + val cat = rawConfig.getCategory(category); + for (val field : configClass.getDeclaredFields()) { + if (field.getAnnotation(Config.Ignore.class) != null) { + continue; + } + field.setAccessible(true); + val comment = Optional.ofNullable(field.getAnnotation(Config.Comment.class)) + .map(Config.Comment::value) + .map((lines) -> String.join("\n", lines)) + .orElse(""); + val name = Optional.ofNullable(field.getAnnotation(Config.Name.class)) + .map(Config.Name::value) + .orElse(field.getName()); + val langKey = Optional.ofNullable(field.getAnnotation(Config.LangKey.class)) + .map(Config.LangKey::value) + .orElse(name); + val fieldClass = field.getType(); + var boxed = false; + if ((boxed = fieldClass.equals(Boolean.class)) || fieldClass.equals(boolean.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultBoolean.class)) + .map(Config.DefaultBoolean::value) + .orElse(boxed ? (Boolean) field.get(null) : field.getBoolean(null)); + field.setBoolean(null, rawConfig.getBoolean(name, category, defaultValue, comment, langKey)); + } else if ((boxed = fieldClass.equals(Integer.class)) || fieldClass.equals(int.class)) { + val range = Optional.ofNullable(field.getAnnotation(Config.RangeInt.class)); + val min = range.map(Config.RangeInt::min).orElse(Integer.MIN_VALUE); + val max = range.map(Config.RangeInt::max).orElse(Integer.MAX_VALUE); + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultInt.class)) + .map(Config.DefaultInt::value) + .orElse(boxed ? (Integer) field.get(null) : field.getInt(null)); + field.setInt(null, rawConfig.getInt(name, category, defaultValue, min, max, comment, langKey)); + } else if ((boxed = fieldClass.equals(Float.class)) || fieldClass.equals(float.class)) { + val range = Optional.ofNullable(field.getAnnotation(Config.RangeFloat.class)); + val min = range.map(Config.RangeFloat::min).orElse(Float.MIN_VALUE); + val max = range.map(Config.RangeFloat::max).orElse(Float.MAX_VALUE); + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultFloat.class)) + .map(Config.DefaultFloat::value) + .orElse(boxed ? (Float) field.get(null) : field.getFloat(null)); + field.setFloat(null, rawConfig.getFloat(name, category, defaultValue, min, max, comment, langKey)); + } else if (fieldClass.equals(String.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultString.class)) + .map(Config.DefaultString::value) + .orElse((String) field.get(null)); + val pattern = Optional.ofNullable(field.getAnnotation(Config.Pattern.class)) + .map(Config.Pattern::value) + .map(Pattern::compile) + .orElse(null); + field.set(null, rawConfig.getString(name, category, defaultValue, comment, langKey, pattern)); + } else if (fieldClass.isEnum()) { + val enumValues = Arrays.stream((Object[]) fieldClass.getDeclaredMethod("values").invoke(null)) + .map((obj) -> (Enum) obj) + .collect(Collectors.toList()); + val defaultValue = (Enum) Optional.ofNullable(field.getAnnotation(Config.DefaultEnum.class)) + .map(Config.DefaultEnum::value) + .map((defName) -> extractField(fieldClass, defName)) + .map(ConfigurationManager::extractValue) + .orElse(field.get(null)); + val possibleValues = enumValues.stream().map(Enum::name).toArray(String[]::new); + var value = rawConfig.getString(name, + category, + defaultValue.name(), + comment + "\nPossible values: " + Arrays.toString(possibleValues) + + "\n", + possibleValues, + langKey); + + try { + if (!Arrays.asList(possibleValues).contains(value)) { + throw new NoSuchFieldException(); + } + val enumField = fieldClass.getDeclaredField(value); + if (!enumField.isEnumConstant()) { + throw new NoSuchFieldException(); + } + field.set(null, enumField.get(null)); + } catch (NoSuchFieldException e) { + LOGGER + .warn("Invalid value " + value + " for enum configuration field " + field.getName() + + " of type " + fieldClass.getName() + " in config class " + + configClass.getName() + "! Using default value of " + defaultValue + "!"); + field.set(null, defaultValue); + } + } else if (fieldClass.isArray() && fieldClass.getComponentType().equals(String.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultStringList.class)).map(Config.DefaultStringList::value).orElse((String[])field.get(null)); + var value = rawConfig.getStringList(name, category, defaultValue, comment, null, langKey); + field.set(null, value); + } else { + throw new ConfigException("Illegal config field: " + field.getName() + " in " + configClass.getName() + + ": Unsupported type " + fieldClass.getName() + + "! Did you forget an @Ignore annotation?"); + } + if (field.isAnnotationPresent(Config.RequiresMcRestart.class)) { + cat.setRequiresMcRestart(true); + } + if (field.isAnnotationPresent(Config.RequiresWorldRestart.class)) { + cat.setRequiresWorldRestart(true); + } + } + } + + @SneakyThrows + private static Field extractField(Class clazz, String field) { + return clazz.getDeclaredField(field); + } + + @SneakyThrows + private static Object extractValue(Field field) { + return field.get(null); + } + + /** + * Process the configuration into a list of config elements usable in config GUI code. + * + * @param configClass The class to process. + * + * @return The configuration elements. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static List getConfigElements(Class configClass) throws ConfigException { + init(); + val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .orElseThrow(() -> new ConfigException( + "Class " + configClass.getName() + " does not have a @Config annotation!")); + val rawConfig = Optional.ofNullable(configs.get(cfg.modid())) + .map((conf) -> Optional.ofNullable(configToClassMap.get(conf)) + .map((l) -> l.contains(configClass)) + .orElse(false) ? conf : null) + .orElseThrow(() -> new ConfigException( + "Tried to get config elements for non-registered config class!")); + val category = cfg.category(); + val elements = new ConfigElement<>(rawConfig.getCategory(category)).getChildElements(); + return elements.stream().map((element) -> new IConfigElementProxy(element, () -> { + try { + processConfigInternal(configClass, category, rawConfig); + rawConfig.save(); + } catch (IllegalAccessException | + NoSuchMethodException | + InvocationTargetException | + NoSuchFieldException | + ConfigException e) { + e.printStackTrace(); + } + })).collect(Collectors.toList()); + } + + private static File minecraftHome() { + return Launch.minecraftHome != null ? Launch.minecraftHome : new File("."); + } + + private static void init() { + if (initialized) { + return; + } + configDir = minecraftHome().toPath().resolve("config"); + initialized = true; + } + + /** + * Internal, do not use. + */ + public static void registerBus() { + FMLCommonHandler.instance().bus().register(instance); + } + + /** + * Internal, do not use. + * + * @param event The event. + */ + @SubscribeEvent + public void onConfigChanged(ConfigChangedEvent.OnConfigChangedEvent event) { + init(); + val config = configs.get(event.modID); + if (config == null) { + return; + } + val configClasses = configToClassMap.get(config); + configClasses.forEach((configClass) -> { + try { + val category = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .map(Config::category) + .orElseThrow(() -> new ConfigException( + "Failed to get config category for class " + configClass.getName())); + processConfigInternal(configClass, category, config); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/IConfigElementProxy.java b/src/main/java/com/gtnewhorizons/angelica/config/IConfigElementProxy.java new file mode 100644 index 000000000..1c4e9638d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/IConfigElementProxy.java @@ -0,0 +1,158 @@ +package com.gtnewhorizons.angelica.config; + +import cpw.mods.fml.client.config.ConfigGuiType; +import cpw.mods.fml.client.config.GuiConfigEntries; +import cpw.mods.fml.client.config.GuiEditArrayEntries; +import cpw.mods.fml.client.config.IConfigElement; + +import java.util.List; +import java.util.regex.Pattern; + +public class IConfigElementProxy implements IConfigElement { + private final IConfigElement proxied; + private final Runnable onUpdate; + + public IConfigElementProxy(IConfigElement proxied, Runnable onUpdate) { + this.proxied = proxied; + this.onUpdate = onUpdate; + } + + @Override + public boolean isProperty() { + return proxied.isProperty(); + } + + @SuppressWarnings("rawtypes") + @Override + public Class getConfigEntryClass() { + return proxied.getConfigEntryClass(); + } + + @Override + public Class getArrayEntryClass() { + return proxied.getArrayEntryClass(); + } + + @Override + public String getName() { + return proxied.getName(); + } + + @Override + public String getQualifiedName() { + return proxied.getQualifiedName(); + } + + @Override + public String getLanguageKey() { + return proxied.getLanguageKey(); + } + + @Override + public String getComment() { + return proxied.getComment(); + } + + @SuppressWarnings("rawtypes") + @Override + public List getChildElements() { + return proxied.getChildElements(); + } + + @Override + public ConfigGuiType getType() { + return proxied.getType(); + } + + @Override + public boolean isList() { + return proxied.isList(); + } + + @Override + public boolean isListLengthFixed() { + return proxied.isListLengthFixed(); + } + + @Override + public int getMaxListLength() { + return proxied.getMaxListLength(); + } + + @Override + public boolean isDefault() { + return proxied.isDefault(); + } + + @Override + public Object getDefault() { + return proxied.getDefault(); + } + + @Override + public Object[] getDefaults() { + return proxied.getDefaults(); + } + + @Override + public void setToDefault() { + proxied.setToDefault(); + } + + @Override + public boolean requiresWorldRestart() { + return proxied.requiresWorldRestart(); + } + + @Override + public boolean showInGui() { + return proxied.showInGui(); + } + + @Override + public boolean requiresMcRestart() { + return proxied.requiresMcRestart(); + } + + @Override + public Object get() { + return proxied.get(); + } + + @Override + public Object[] getList() { + return proxied.getList(); + } + + @Override + public void set(T value) { + proxied.set(value); + onUpdate.run(); + } + + @Override + public void set(T[] aVal) { + proxied.set(aVal); + onUpdate.run(); + } + + @Override + public String[] getValidValues() { + return proxied.getValidValues(); + } + + @Override + public T getMinValue() { + return proxied.getMinValue(); + } + + @Override + public T getMaxValue() { + return proxied.getMaxValue(); + } + + @Override + public Pattern getValidationPattern() { + return proxied.getValidationPattern(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiConfig.java b/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiConfig.java new file mode 100644 index 000000000..8365be361 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiConfig.java @@ -0,0 +1,13 @@ +package com.gtnewhorizons.angelica.config; + +import cpw.mods.fml.client.config.GuiConfig; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.GuiScreen; + +@SideOnly(Side.CLIENT) +public class SimpleGuiConfig extends GuiConfig { + public SimpleGuiConfig(GuiScreen parent, Class configClass, String modID, String modName) throws ConfigException { + super(parent, ConfigurationManager.getConfigElements(configClass), modID, false, false, modName + " Configuration"); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiFactory.java b/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiFactory.java new file mode 100644 index 000000000..9d8560715 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/config/SimpleGuiFactory.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.config; + +import cpw.mods.fml.client.IModGuiFactory; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; + +import java.util.Set; + +@SideOnly(Side.CLIENT) +public interface SimpleGuiFactory extends IModGuiFactory { + @Override + default void initialize(Minecraft minecraftInstance) { + + } + + @Override + default Set runtimeGuiCategories() { + return null; + } + + @Override + default RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) { + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/Dirty.java b/src/main/java/com/gtnewhorizons/angelica/glsm/Dirty.java new file mode 100644 index 000000000..0cb15815a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/Dirty.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +public class Dirty { + private Dirty() {} + + public static final long + ALPHA_TEST = 1L << 0, + BLEND = 1L << 1, + DEPTH_TEST = 1L << 2, + CULL = 1L << 3, + LIGHTING = 1L << 4, + RESCALE_NORMAL = 1L << 5, + TEXTURE_2D = 1L << 6, + FOG = 1L << 7, + BLEND_STATE = 1L << 8, + DEPTH_FUNC = 1L << 9, + DEPTH_MASK = 1L << 10, + COLOR = 1L << 11, + COLOR_MASK = 1L << 12, + CLEAR_COLOR = 1L << 13, + ACTIVE_TEXTURE = 1L << 14, + BOUND_TEXTURE = 1L << 15, + SHADE_MODEL = 1L << 16, + ALL = ALPHA_TEST | BLEND | DEPTH_TEST | CULL | LIGHTING | RESCALE_NORMAL | TEXTURE_2D | FOG | BLEND_STATE | DEPTH_FUNC | DEPTH_MASK | COLOR | COLOR_MASK | CLEAR_COLOR | ACTIVE_TEXTURE | BOUND_TEXTURE | SHADE_MODEL; + + public static long getFlagFromCap(int cap) { + return switch (cap) { + case GL11.GL_ALPHA_TEST -> ALPHA_TEST; + case GL11.GL_BLEND -> BLEND; + case GL11.GL_DEPTH_TEST -> DEPTH_TEST; + case GL11.GL_CULL_FACE -> CULL; + case GL11.GL_LIGHTING -> LIGHTING; + case GL12.GL_RESCALE_NORMAL -> RESCALE_NORMAL; + case GL11.GL_TEXTURE_2D -> TEXTURE_2D; + case GL11.GL_FOG -> FOG; + default -> throw new RuntimeException("Invalid cap: " + cap); + }; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java b/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java new file mode 100644 index 000000000..c996f19ac --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java @@ -0,0 +1,697 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.states.AlphaState; +import com.gtnewhorizons.angelica.glsm.states.BlendState; +import com.gtnewhorizons.angelica.glsm.states.BooleanState; +import com.gtnewhorizons.angelica.glsm.states.Color4; +import com.gtnewhorizons.angelica.glsm.states.DepthState; +import com.gtnewhorizons.angelica.glsm.states.FogState; +import com.gtnewhorizons.angelica.glsm.states.GLColorMask; +import com.gtnewhorizons.angelica.glsm.states.TextureState; +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import lombok.Getter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gbuffer_overrides.state.StateTracker; +import net.coderbot.iris.gl.blending.AlphaTestStorage; +import net.coderbot.iris.gl.blending.BlendModeStorage; +import net.coderbot.iris.gl.blending.DepthColorStorage; +import net.coderbot.iris.gl.sampler.SamplerLimits; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.samplers.IrisSamplers; +import net.coderbot.iris.texture.TextureInfoCache; +import net.coderbot.iris.texture.TextureTracker; +import net.coderbot.iris.texture.pbr.PBRTextureManager; +import net.minecraft.client.renderer.OpenGlHelper; +import org.joml.Vector3d; +import org.lwjgl.LWJGLException; +import org.lwjgl.opengl.ARBMultitexture; +import org.lwjgl.opengl.Drawable; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL13; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.stream.IntStream; + +import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER; + +@SuppressWarnings("unused") // Used in ASM +public class GLStateManager { + public static final boolean BYPASS_CACHE = Boolean.parseBoolean(System.getProperty("angelica.disableGlCache", "false")); + + // GLStateManager State Trackers + @Getter private static int activeTexture; + @Getter private static final BlendState blendState = new BlendState(); + @Getter private static final DepthState depthState = new DepthState(); + @Getter private static final FogState fogState = new FogState(); + @Getter private static final Color4 Color = new Color4(); + @Getter private static final Color4 ClearColor = new Color4(); + @Getter private static final GLColorMask ColorMask = new GLColorMask(); + @Getter private static final BooleanState cullState = new BooleanState(GL11.GL_CULL_FACE); + @Getter private static final AlphaState alphaState = new AlphaState(); + @Getter private static final BooleanState lightingState = new BooleanState(GL11.GL_LIGHTING); + @Getter private static final BooleanState rescaleNormalState = new BooleanState(GL12.GL_RESCALE_NORMAL); + + private static long dirty = 0; + + private static int modelShadeMode; + + @Getter + private static TextureState[] Textures; + + // Iris Listeners + private static Runnable blendFuncListener = null; + private static Runnable fogToggleListener = null; + private static Runnable fogModeListener = null; + private static Runnable fogStartListener = null; + private static Runnable fogEndListener = null; + private static Runnable fogDensityListener = null; + + // Thread Checking + @Getter + private static final Thread MainThread = Thread.currentThread(); + private static Thread CurrentThread = MainThread; + private static boolean runningSplash = false; + + private static boolean inGLNewList = false; + + private static boolean hudCaching$blendEnabled; + + public static void init() { + if (AngelicaConfig.enableIris) { + StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener; + StateUpdateNotifiers.fogToggleNotifier = listener -> fogToggleListener = listener; + StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener; + StateUpdateNotifiers.fogStartNotifier = listener -> fogStartListener = listener; + StateUpdateNotifiers.fogEndNotifier = listener -> fogEndListener = listener; + StateUpdateNotifiers.fogDensityNotifier = listener -> fogDensityListener = listener; + } + // We want textures regardless of Iris being initialized, and using SamplerLimits is isolated enough + Textures = IntStream.range(0, SamplerLimits.get().getMaxTextureUnits()).mapToObj(i -> new TextureState()).toArray(TextureState[]::new); + } + + public static void assertMainThread() { + if (Thread.currentThread() != CurrentThread && !runningSplash) { + LOGGER.info("Call from not the Current Thread! - " + Thread.currentThread().getName() + " Current thread: " + CurrentThread.getName()); + } + } + + // LWJGL Overrides + public static void glEnable(int cap) { + switch (cap) { + case GL11.GL_ALPHA_TEST -> enableAlphaTest(); + case GL11.GL_BLEND -> enableBlend(); + case GL11.GL_DEPTH_TEST -> enableDepthTest(); + case GL11.GL_CULL_FACE -> enableCull(); + case GL11.GL_LIGHTING -> enableLighting(); + case GL12.GL_RESCALE_NORMAL -> enableRescaleNormal(); + case GL11.GL_TEXTURE_2D -> enableTexture(); + case GL11.GL_FOG -> enableFog(); + default -> GL11.glEnable(cap); + } + } + + public static void glDisable(int cap) { + switch (cap) { + case GL11.GL_ALPHA_TEST -> disableAlphaTest(); + case GL11.GL_BLEND -> disableBlend(); + case GL11.GL_DEPTH_TEST -> disableDepthTest(); + case GL11.GL_CULL_FACE -> disableCull(); + case GL11.GL_LIGHTING -> disableLighting(); + case GL12.GL_RESCALE_NORMAL -> disableRescaleNormal(); + case GL11.GL_TEXTURE_2D -> disableTexture(); + case GL11.GL_FOG -> disableFog(); + default -> GL11.glDisable(cap); + } + } + + // GLStateManager Functions + + public static void enableBlend() { + if (AngelicaConfig.enableIris) { + if (BlendModeStorage.isBlendLocked()) { + BlendModeStorage.deferBlendModeToggle(true); + return; + } + } + hudCaching$blendEnabled = true; + blendState.mode.enable(); + } + + public static void disableBlend() { + if (AngelicaConfig.enableIris) { + if (BlendModeStorage.isBlendLocked()) { + BlendModeStorage.deferBlendModeToggle(false); + return; + } + } + hudCaching$blendEnabled = false; + blendState.mode.disable(); + } + + public static void glBlendFunc(int srcFactor, int dstFactor) { + if (AngelicaConfig.enableIris) { + if (BlendModeStorage.isBlendLocked()) { + BlendModeStorage.deferBlendFunc(srcFactor, dstFactor, srcFactor, dstFactor); + return; + } + } + if (HUDCaching.renderingCacheOverride) { + blendState.srcRgb = srcFactor; + blendState.dstRgb = dstFactor; + blendState.srcAlpha = GL11.GL_ONE; + blendState.dstAlpha = GL11.GL_ONE_MINUS_SRC_ALPHA; + OpenGlHelper.glBlendFunc(srcFactor, dstFactor, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + clean(Dirty.BLEND_STATE); + return; + } + if (GLStateManager.BYPASS_CACHE || blendState.srcRgb != srcFactor || blendState.dstRgb != dstFactor || checkDirty(Dirty.BLEND_STATE)) { + blendState.srcRgb = srcFactor; + blendState.dstRgb = dstFactor; + GL11.glBlendFunc(srcFactor, dstFactor); + } + + // Iris + if (blendFuncListener != null) blendFuncListener.run(); + } + + public static void tryBlendFuncSeparate(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + if (AngelicaConfig.enableIris) { + if (BlendModeStorage.isBlendLocked()) { + BlendModeStorage.deferBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + return; + } + } + if (HUDCaching.renderingCacheOverride && dstAlpha != GL11.GL_ONE_MINUS_SRC_ALPHA) { + srcAlpha = GL11.GL_ONE; + dstAlpha = GL11.GL_ONE_MINUS_SRC_ALPHA; + } + if (GLStateManager.BYPASS_CACHE || blendState.srcRgb != srcRgb || blendState.dstRgb != dstRgb || blendState.srcAlpha != srcAlpha || blendState.dstAlpha != dstAlpha || checkDirty(Dirty.BLEND_STATE)) { + blendState.srcRgb = srcRgb; + blendState.dstRgb = dstRgb; + blendState.srcAlpha = srcAlpha; + blendState.dstAlpha = dstAlpha; + OpenGlHelper.glBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + } + + // Iris + if (blendFuncListener != null) blendFuncListener.run(); + } + + public static void glDepthFunc(int func) { + // Hacky workaround for now, need to figure out why this isn't being applied... + if (GLStateManager.BYPASS_CACHE || func != depthState.func || GLStateManager.runningSplash || checkDirty(Dirty.DEPTH_FUNC)) { + depthState.func = func; + GL11.glDepthFunc(func); + } + } + + public static void glDepthMask(boolean mask) { + if (AngelicaConfig.enableIris) { + if (DepthColorStorage.isDepthColorLocked()) { + DepthColorStorage.deferDepthEnable(mask); + return; + } + } + + if (mask != depthState.mask || checkDirty(Dirty.DEPTH_MASK)) { + depthState.mask = mask; + GL11.glDepthMask(mask); + } + } + + public static void glColor4f(float red, float green, float blue, float alpha) { + if (!hudCaching$blendEnabled && HUDCaching.renderingCacheOverride && alpha < 1f) { + alpha = 1f; + } + if (changeColor(red, green, blue, alpha)) { + GL11.glColor4f(red, green, blue, alpha); + } + } + + public static void glColor4d(double red, double green, double blue, double alpha) { + if (!hudCaching$blendEnabled && HUDCaching.renderingCacheOverride && alpha < 1d) { + alpha = 1d; + } + if (changeColor((float) red, (float) green, (float) blue, (float) alpha)) { + GL11.glColor4d(red, green, blue, alpha); + } + } + + public static void glColor4b(byte red, byte green, byte blue, byte alpha) { + if (!hudCaching$blendEnabled && HUDCaching.renderingCacheOverride && alpha < Byte.MAX_VALUE) { + alpha = Byte.MAX_VALUE; + } + if (changeColor(b2f(red), b2f(green), b2f(blue), b2f(alpha))) { + GL11.glColor4b(red, green, blue, alpha); + } + } + + public static void glColor4ub(byte red, byte green, byte blue, byte alpha) { + if (!hudCaching$blendEnabled && HUDCaching.renderingCacheOverride && alpha < Byte.MAX_VALUE) { + alpha = Byte.MAX_VALUE; + } + if (changeColor(ub2f(red), ub2f(green), ub2f(blue), ub2f(alpha))) { + GL11.glColor4ub(red, green, blue, alpha); + } + } + + public static void glColor3f(float red, float green, float blue) { + if (changeColor(red, green, blue, 1.0F)) { + GL11.glColor3f(red, green, blue); + } + } + + public static void glColor3d(double red, double green, double blue) { + if (changeColor((float) red, (float) green, (float) blue, 1.0F)) { + GL11.glColor3d(red, green, blue); + } + } + + public static void glColor3b(byte red, byte green, byte blue) { + if (changeColor(b2f(red), b2f(green), b2f(blue), 1.0F)) { + GL11.glColor3b(red, green, blue); + } + } + + public static void glColor3ub(byte red, byte green, byte blue) { + if (changeColor(ub2f(red), ub2f(green), ub2f(blue), 1.0F)) { + GL11.glColor3ub(red, green, blue); + } + } + + private static float ub2f(byte b) { + return (b & 0xFF) / 255.0F; + } + + private static float b2f(byte b) { + return ((b - Byte.MIN_VALUE) & 0xFF) / 255.0F; + } + + private static boolean changeColor(float red, float green, float blue, float alpha) { + // Helper function for glColor* + if (GLStateManager.BYPASS_CACHE || red != Color.red || green != Color.green || blue != Color.blue || alpha != Color.alpha || checkDirty(Dirty.COLOR)) { + Color.red = red; + Color.green = green; + Color.blue = blue; + Color.alpha = alpha; + return true; + } + return false; + } + + public static void clearCurrentColor() { + // Marks the cache dirty, doesn't actually reset the color + dirty(Dirty.COLOR); + } + + public static void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) { + if (AngelicaConfig.enableIris) { + if (DepthColorStorage.isDepthColorLocked()) { + DepthColorStorage.deferColorMask(red, green, blue, alpha); + return; + } + } + if (GLStateManager.BYPASS_CACHE || red != ColorMask.red || green != ColorMask.green || blue != ColorMask.blue || alpha != ColorMask.alpha || checkDirty(Dirty.COLOR_MASK)) { + ColorMask.red = red; + ColorMask.green = green; + ColorMask.blue = blue; + ColorMask.alpha = alpha; + GL11.glColorMask(red, green, blue, alpha); + } + } + + // Clear Color + public static void glClearColor(float red, float green, float blue, float alpha) { + if (GLStateManager.BYPASS_CACHE || red != ClearColor.red || green != ClearColor.green || blue != ClearColor.blue || alpha != ClearColor.alpha || checkDirty(Dirty.CLEAR_COLOR)) { + ClearColor.red = red; + ClearColor.green = green; + ClearColor.blue = blue; + ClearColor.alpha = alpha; + GL11.glClearColor(red, green, blue, alpha); + } + } + + // ALPHA + public static void enableAlphaTest() { + if (AngelicaConfig.enableIris) { + if (AlphaTestStorage.isAlphaTestLocked()) { + AlphaTestStorage.deferAlphaTestToggle(true); + return; + } + } + alphaState.mode.enable(); + } + + public static void disableAlphaTest() { + if (AngelicaConfig.enableIris) { + if (AlphaTestStorage.isAlphaTestLocked()) { + AlphaTestStorage.deferAlphaTestToggle(false); + return; + } + } + alphaState.mode.disable(); + } + + public static void glAlphaFunc(int function, float reference) { + if (AngelicaConfig.enableIris) { + if (AlphaTestStorage.isAlphaTestLocked()) { + AlphaTestStorage.deferAlphaFunc(function, reference); + return; + } + } + alphaState.function = function; + alphaState.reference = reference; + GL11.glAlphaFunc(function, reference); + } + + // Textures + public static void glActiveTexture(int texture) { + final int newTexture = texture - GL13.GL_TEXTURE0; + if (GLStateManager.BYPASS_CACHE || activeTexture != newTexture || checkDirty(Dirty.ACTIVE_TEXTURE)) { + activeTexture = newTexture; + GL13.glActiveTexture(texture); + } + } + + public static void glActiveTextureARB(int texture) { + final int newTexture = texture - GL13.GL_TEXTURE0; + if (GLStateManager.BYPASS_CACHE || activeTexture != newTexture || checkDirty(Dirty.ACTIVE_TEXTURE)) { + activeTexture = newTexture; + ARBMultitexture.glActiveTextureARB(texture); + } + } + + public static void glBindTexture(int target, int texture) { + if(inGLNewList) { + // Binding a texture, while building a list, is not allowed and is a silent noop + Throwable throwable = new Throwable(); + LOGGER.info("Naughty naughty, someone's making a texture binding in a display list!", throwable); + return; + } + + // TODO: Do we need to do the check dirty for each bound texture? + if (GLStateManager.BYPASS_CACHE || Textures[activeTexture].binding != texture || runningSplash || checkDirty(Dirty.BOUND_TEXTURE)) { + GL11.glBindTexture(target, texture); + + Textures[activeTexture].binding = texture; + if (AngelicaConfig.enableIris) { + TextureTracker.INSTANCE.onBindTexture(texture); + } + } + } + + public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) { + if (AngelicaConfig.enableIris) { + // Iris + TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } + + public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { + if (AngelicaConfig.enableIris) { + TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : (IntBuffer) null); + } + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } + + public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) { + if (AngelicaConfig.enableIris) { + TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset); + } + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset); + } + + public static void glDeleteTextures(int id) { + if (AngelicaConfig.enableIris) { + iris$onDeleteTexture(id); + } + Textures[activeTexture].binding = -1; + GL11.glDeleteTextures(id); + } + + public static void glDeleteTextures(IntBuffer ids) { + if (AngelicaConfig.enableIris) { + for(int i = 0; i < ids.capacity(); i++) { + iris$onDeleteTexture(ids.get(i)); + } + } + Textures[activeTexture].binding = -1; + GL11.glDeleteTextures(ids); + } + + public static void enableTexture() { + if (AngelicaConfig.enableIris) { + // Iris + boolean updatePipeline = false; + if (activeTexture == IrisSamplers.ALBEDO_TEXTURE_UNIT) { + StateTracker.INSTANCE.albedoSampler = true; + updatePipeline = true; + } else if (activeTexture == IrisSamplers.LIGHTMAP_TEXTURE_UNIT) { + StateTracker.INSTANCE.lightmapSampler = true; + updatePipeline = true; + } else if (activeTexture == IrisSamplers.OVERLAY_TEXTURE_UNIT) { + StateTracker.INSTANCE.overlaySampler = true; + updatePipeline = true; + } + + if (updatePipeline) { + Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setInputs(StateTracker.INSTANCE.getInputs())); + } + } + Textures[activeTexture].mode.enable(); + } + + public static void disableTexture() { + if (AngelicaConfig.enableIris) { + // Iris + boolean updatePipeline = false; + if (activeTexture == IrisSamplers.ALBEDO_TEXTURE_UNIT) { + StateTracker.INSTANCE.albedoSampler = false; + updatePipeline = true; + } else if (activeTexture == IrisSamplers.LIGHTMAP_TEXTURE_UNIT) { + StateTracker.INSTANCE.lightmapSampler = false; + updatePipeline = true; + } else if (activeTexture == IrisSamplers.OVERLAY_TEXTURE_UNIT) { + StateTracker.INSTANCE.overlaySampler = false; + updatePipeline = true; + } + + if (updatePipeline) { + Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setInputs(StateTracker.INSTANCE.getInputs())); + } + } + Textures[activeTexture].mode.disable(); + } + + public static void setFilter(boolean bilinear, boolean mipmap) { + int i, j; + if (bilinear) { + i = mipmap ? GL11.GL_LINEAR_MIPMAP_LINEAR : GL11.GL_LINEAR; + j = GL11.GL_LINEAR; + } else { + i = mipmap ? GL11.GL_NEAREST_MIPMAP_LINEAR : GL11.GL_NEAREST; + j = GL11.GL_NEAREST; + } + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, i); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, j); + } + + public static void glDrawArrays(int mode, int first, int count) { + // Iris -- TODO: This doesn't seem to work and is related to matchPass() + Iris.getPipelineManager().getPipeline().ifPresent(WorldRenderingPipeline::syncProgram); + GL11.glDrawArrays(mode, first, count); + } + + public static void defaultBlendFunc() { + tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); + } + + public static void enableCull() { + cullState.enable(); + } + + public static void disableCull() { + cullState.disable(); + } + + public static void enableDepthTest() { + depthState.mode.enable(); + } + + public static void disableDepthTest() { + depthState.mode.disable(); + } + + public static void enableLighting() { + lightingState.enable(); + } + + public static void disableLighting() { + lightingState.disable(); + } + + public static void enableRescaleNormal() { + rescaleNormalState.enable(); + } + + public static void disableRescaleNormal() { + rescaleNormalState.disable(); + } + + public static void enableFog() { + fogState.mode.enable(); + if (fogToggleListener != null) { + fogToggleListener.run(); + } + } + + public static void disableFog() { + fogState.mode.disable(); + if (fogToggleListener != null) { + fogToggleListener.run(); + } + } + + public static void glFog(int pname, FloatBuffer param) { + // TODO: Iris Notifier + GL11.glFog(pname, param); + if (pname == GL11.GL_FOG_COLOR) { + final float red = param.get(0); + final float green = param.get(1); + final float blue = param.get(2); + + fogState.fogColor.set(red, green, blue); + fogState.fogAlpha = param.get(3); + fogState.fogColorBuffer.clear(); + fogState.fogColorBuffer.put((FloatBuffer) param.position(0)).flip(); + } + } + + public static Vector3d getFogColor() { + return fogState.fogColor; + } + + public static void fogColor(float red, float green, float blue, float alpha) { + if (GLStateManager.BYPASS_CACHE || red != fogState.fogColor.x || green != fogState.fogColor.y || blue != fogState.fogColor.z || alpha != fogState.fogAlpha) { + fogState.fogColor.set(red, green, blue); + fogState.fogAlpha = alpha; + fogState.fogColorBuffer.clear(); + fogState.fogColorBuffer.put(red).put(green).put(blue).put(alpha).flip(); + GL11.glFog(GL11.GL_FOG_COLOR, fogState.fogColorBuffer); + } + } + + public static void glFogf(int pname, float param) { + GL11.glFogf(pname, param); + switch (pname) { + case GL11.GL_FOG_DENSITY -> { + fogState.density = param; + if (fogDensityListener != null) { + fogDensityListener.run(); + } + } + case GL11.GL_FOG_START -> { + fogState.start = param; + if (fogStartListener != null) { + fogStartListener.run(); + } + } + case GL11.GL_FOG_END -> { + fogState.end = param; + if (fogEndListener != null) { + fogEndListener.run(); + } + } + } + } + + public static void glFogi(int pname, int param) { + GL11.glFogi(pname, param); + if (pname == GL11.GL_FOG_MODE) { + fogState.fogMode = param; + if (fogModeListener != null) { + fogModeListener.run(); + } + } + } + + public static void setFogBlack() { + glFogf(GL11.GL_FOG_COLOR, 0.0F); + } + + public static void glShadeModel(int mode) { + if (GLStateManager.BYPASS_CACHE || modelShadeMode != mode || checkDirty(Dirty.SHADE_MODEL)) { + modelShadeMode = mode; + GL11.glShadeModel(mode); + } + } + + // Iris Functions + private static void iris$onDeleteTexture(int id) { + if (AngelicaConfig.enableIris) { + TextureTracker.INSTANCE.onDeleteTexture(id); + TextureInfoCache.INSTANCE.onDeleteTexture(id); + PBRTextureManager.INSTANCE.onDeleteTexture(id); + } + } + + public static void setRunningSplash(boolean runningSplash) { + GLStateManager.runningSplash = runningSplash; + } + + public static void makeCurrent(Drawable drawable) throws LWJGLException { + drawable.makeCurrent(); + final Thread currentThread = Thread.currentThread(); + + CurrentThread = currentThread; + LOGGER.info("Current thread: {}", currentThread.getName()); + } + + public static void glNewList(int list, int mode) { + if(inGLNewList) { + throw new RuntimeException("glNewList called inside of a display list!"); + } + inGLNewList = true; + GL11.glNewList(list, mode); + } + + public static void glEndList() { + if(!inGLNewList) { + throw new RuntimeException("glEndList called outside of a display list!"); + } + inGLNewList = false; + GL11.glEndList(); + } + + public static void glPushAttrib(int mask) { + // TODO: Proper state tracking; but at least for now the current cases of this didn't do anything related to textures and + // just overly broadly set ALL_BITS :facepalm: + GL11.glPushAttrib(mask & ~(GL11.GL_TEXTURE_BIT)); + } + + public static void glPopAttrib() { + dirty = Dirty.ALL; + GL11.glPopAttrib(); + } + + public static boolean checkDirty(long flag) { + if((dirty & flag ) == flag) { + dirty &= ~flag; + return true; + } + return false; + } + public static void clean(long dirtyFlag) { + dirty &= ~dirtyFlag; + } + + public static void dirty(long dirtyFlag) { + dirty |= dirtyFlag; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/TessellatorManager.java b/src/main/java/com/gtnewhorizons/angelica/glsm/TessellatorManager.java new file mode 100644 index 000000000..e99513170 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/TessellatorManager.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.glsm; + +import net.minecraft.client.renderer.Tessellator; + +public class TessellatorManager { + private static final ThreadLocal theTessellator = ThreadLocal.withInitial(Tessellator::new); + private static final Thread mainThread = Thread.currentThread(); + + public static Tessellator get() { + if(isOnMainThread()) + return Tessellator.instance; + return theTessellator.get(); + } + + public static boolean isOnMainThread() { + return Thread.currentThread() == mainThread; + } + + public static boolean isMainInstance(Object instance) { + return instance == Tessellator.instance; + } + + static { + System.out.println("[TessellatorManager] Initialized on thread " + mainThread.getName()); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/ThreadedBlockData.java b/src/main/java/com/gtnewhorizons/angelica/glsm/ThreadedBlockData.java new file mode 100644 index 000000000..843b6c0d2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/ThreadedBlockData.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.angelica.glsm; + +import net.minecraft.block.Block; + +/** + * Used to store the block bounds fields in a thread-safe manner, as instance fields don't work correctly + * on multiple threads. + */ +public class ThreadedBlockData { + public double minX, minY, minZ, maxX, maxY, maxZ; + + public ThreadedBlockData() {} + + public ThreadedBlockData(ThreadedBlockData other) { + this.minX = other.minX; + this.minY = other.minY; + this.minZ = other.minZ; + this.maxX = other.maxX; + this.maxY = other.maxY; + this.maxZ = other.maxZ; + } + + public static ThreadedBlockData get(Block block) { + return ((Getter)block).angelica$getThreadData(); + } + + public interface Getter { + ThreadedBlockData angelica$getThreadData(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/AlphaState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/AlphaState.java new file mode 100644 index 000000000..40f5cc39f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/AlphaState.java @@ -0,0 +1,9 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import org.lwjgl.opengl.GL11; + +public class AlphaState { + public final BooleanState mode = new BooleanState(GL11.GL_ALPHA_TEST); + public int function = GL11.GL_ALWAYS; + public float reference = -1.0F; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java new file mode 100644 index 000000000..14a17edee --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java @@ -0,0 +1,13 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import org.lwjgl.opengl.GL11; + +public class BlendState { + + public final BooleanState mode = new BooleanState(GL11.GL_BLEND); + public int srcRgb = GL11.GL_ONE; + public int dstRgb = GL11.GL_ZERO; + public int srcAlpha = GL11.GL_ONE; + public int dstAlpha = GL11.GL_ZERO; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java new file mode 100644 index 000000000..4a2aee43e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import com.gtnewhorizons.angelica.glsm.Dirty; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; +import org.lwjgl.opengl.GL11; + +public class BooleanState { + private final int cap; + + private final long dirtyFlag; + + @Getter + private boolean enabled; + + public BooleanState(int cap) { + this.cap = cap; + this.dirtyFlag = Dirty.getFlagFromCap(this.cap); + } + + public void disable() { + this.setEnabled(false); + } + + public void enable() { + this.setEnabled(true); + } + + public void setEnabled(boolean enabled) { + if (GLStateManager.BYPASS_CACHE || enabled != this.enabled || GLStateManager.checkDirty(this.dirtyFlag)) { + this.enabled = enabled; + if (enabled) { + GL11.glEnable(this.cap); + } else { + GL11.glDisable(this.cap); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/Color4.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/Color4.java new file mode 100644 index 000000000..c86b0af44 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/Color4.java @@ -0,0 +1,9 @@ +package com.gtnewhorizons.angelica.glsm.states; + +public class Color4 { + public float red = 1.0F; + public float green = 1.0F; + public float blue = 1.0F; + public float alpha = 1.0F; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/DepthState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/DepthState.java new file mode 100644 index 000000000..905e9930c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/DepthState.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import org.lwjgl.opengl.GL11; + +public class DepthState { + public final BooleanState mode = new BooleanState(GL11.GL_DEPTH_TEST); + public boolean mask = true; + public int func = GL11.GL_LESS; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java new file mode 100644 index 000000000..a2618f38c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import org.joml.Vector3d; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; + +public class FogState { + public BooleanState mode = new BooleanState(GL11.GL_FOG); + public int fogMode = GL11.GL_EXP; + public final Vector3d fogColor = new Vector3d(0.0F, 0.0F, 0.0F); + public float fogAlpha = 1.0F; + public final FloatBuffer fogColorBuffer = FloatBuffer.allocate(4); + public float density = 1.0F; + public float start; + public float end = 1.0F; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/GLColorMask.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/GLColorMask.java new file mode 100644 index 000000000..b656c775b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/GLColorMask.java @@ -0,0 +1,9 @@ +package com.gtnewhorizons.angelica.glsm.states; + +public class GLColorMask { + public boolean red = true; + public boolean green = true; + public boolean blue = true; + public boolean alpha = true; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/TextureState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/TextureState.java new file mode 100644 index 000000000..36709a8f2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/TextureState.java @@ -0,0 +1,8 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import org.lwjgl.opengl.GL11; + +public class TextureState { + public final BooleanState mode = new BooleanState(GL11.GL_TEXTURE_2D); + public int binding; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/helpers/LoadControllerHelper.java b/src/main/java/com/gtnewhorizons/angelica/helpers/LoadControllerHelper.java new file mode 100644 index 000000000..92a906696 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/helpers/LoadControllerHelper.java @@ -0,0 +1,177 @@ +package com.gtnewhorizons.angelica.helpers; + +import com.google.common.collect.ListMultimap; +import com.google.common.eventbus.EventBus; +import cpw.mods.fml.common.LoadController; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.MetadataCollection; +import cpw.mods.fml.common.ModContainer; +import cpw.mods.fml.common.ModMetadata; +import cpw.mods.fml.common.ObfuscationReflectionHelper; +import cpw.mods.fml.common.versioning.ArtifactVersion; +import cpw.mods.fml.common.versioning.VersionRange; + +import java.io.File; +import java.security.cert.Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class LoadControllerHelper { + private static LoadController loadController; + private static ListMultimap packageOwners; + + static { + loadController = ObfuscationReflectionHelper.getPrivateValue(Loader.class, Loader.instance(), "modController"); + packageOwners = ObfuscationReflectionHelper.getPrivateValue(LoadController.class, loadController, "packageOwners"); + } + + private static ConcurrentHashMap, ModContainer> owningModForClass = new ConcurrentHashMap<>(); + + public static ModContainer getOwningMod(Class clz) { + ModContainer container = owningModForClass.computeIfAbsent(clz, c -> { + if(clz.getName().startsWith("net.minecraft.")) + return Loader.instance().getMinecraftModContainer(); + int lastDot = clz.getName().lastIndexOf('.'); + if(lastDot == -1) + return NONE; + String pkgName = clz.getName().substring(0, lastDot); + if(packageOwners.containsKey(pkgName)) + return packageOwners.get(pkgName).get(0); + else + return NONE; + }); + if(container == NONE) + return null; + return container; + } + + private static final ModContainer NONE = new ModContainer() { + @Override + public String getModId() { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public String getVersion() { + return null; + } + + @Override + public File getSource() { + return null; + } + + @Override + public ModMetadata getMetadata() { + return null; + } + + @Override + public void bindMetadata(MetadataCollection mc) { + + } + + @Override + public void setEnabledState(boolean enabled) { + + } + + @Override + public Set getRequirements() { + return null; + } + + @Override + public List getDependencies() { + return null; + } + + @Override + public List getDependants() { + return null; + } + + @Override + public String getSortingRules() { + return null; + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + return false; + } + + @Override + public boolean matches(Object mod) { + return false; + } + + @Override + public Object getMod() { + return null; + } + + @Override + public ArtifactVersion getProcessedVersion() { + return null; + } + + @Override + public boolean isImmutable() { + return false; + } + + @Override + public String getDisplayVersion() { + return null; + } + + @Override + public VersionRange acceptableMinecraftVersionRange() { + return null; + } + + @Override + public Certificate getSigningCertificate() { + return null; + } + + @Override + public Map getCustomModProperties() { + return null; + } + + @Override + public Class getCustomResourcePackClass() { + return null; + } + + @Override + public Map getSharedModDescriptor() { + return null; + } + + @Override + public Disableable canBeDisabled() { + return null; + } + + @Override + public String getGuiClassName() { + return null; + } + + @Override + public List getOwnedPackages() { + return null; + } + }; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/hudcaching/HUDCaching.java b/src/main/java/com/gtnewhorizons/angelica/hudcaching/HUDCaching.java new file mode 100644 index 000000000..855a7c830 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/hudcaching/HUDCaching.java @@ -0,0 +1,178 @@ +package com.gtnewhorizons.angelica.hudcaching; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.TessellatorManager; +import com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching.GuiIngameForgeAccessor; +import cpw.mods.fml.client.registry.ClientRegistry; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.InputEvent; +import cpw.mods.fml.common.gameevent.TickEvent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiIngame; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.util.ChatComponentText; +import net.minecraftforge.client.GuiIngameForge; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +// See LICENSE+HUDCaching.md for license information. + +public class HUDCaching { + + private static final Minecraft mc = Minecraft.getMinecraft(); + public static Framebuffer framebuffer; + private static boolean dirty = true; + public static boolean renderingCacheOverride; + public static final HUDCaching INSTANCE = new HUDCaching(); + + + private HUDCaching() {} + + /* TODO START REMOVE DEBUG STUFF */ + + private final List updateTimeList = new ArrayList<>(21); + private static boolean isEnabled = true; + private static final KeyBinding toggle = new KeyBinding("Toggle HUDCaching", 0, "Debug"); + + static { + ClientRegistry.registerKeyBinding(toggle); + } + +// @SubscribeEvent +// public void onFrame(RenderGameOverlayEvent.Post event) { +// if (event.type == RenderGameOverlayEvent.ElementType.TEXT) { +// final long currentTimeMillis = System.currentTimeMillis(); +// updateTimeList.removeIf(time -> currentTimeMillis - time > 1000L); +// String text = EnumChatFormatting.GREEN + "HUD Fps : " + updateTimeList.size(); +// mc.fontRenderer.drawStringWithShadow( +// text, +// event.resolution.getScaledWidth() / 4, +// event.resolution.getScaledHeight() / 4, +// 0xFFFFFF); +// updateTimeList.add(currentTimeMillis); +// } +// } + + @SubscribeEvent + public void onKeypress(InputEvent.KeyInputEvent event) { + if (toggle.isPressed()) { + isEnabled = !isEnabled; + final String msg = isEnabled ? "Enabled HUDCaching" : "Disabled HUDCaching"; + if (mc.thePlayer != null) mc.thePlayer.addChatMessage(new ChatComponentText(msg)); + } + } + + /* TODO END REMOVE DEBUG STUFF */ + + @SubscribeEvent + public void onTick(TickEvent.ClientTickEvent event) { + if (event.phase == TickEvent.Phase.END) { + dirty = true; + } + } + + // TODO draw vignette + + @SuppressWarnings("unused") + public static void renderCachedHud(EntityRenderer renderer, GuiIngame ingame, float partialTicks, boolean b, int i, int j) { + + if (!OpenGlHelper.isFramebufferEnabled() || !isEnabled) { + ingame.renderGameOverlay(partialTicks, b, i, j); + return; + } + + GLStateManager.enableDepthTest(); + ScaledResolution resolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int width = resolution.getScaledWidth(); + int height = resolution.getScaledHeight(); + renderer.setupOverlayRendering(); + GLStateManager.enableBlend(); + + if (framebuffer != null) { + Tessellator tessellator = TessellatorManager.get(); + if (ingame instanceof GuiIngameForge) { + ((GuiIngameForgeAccessor) ingame).callRenderCrosshairs(width, height); + } else if (GuiIngameForge.renderCrosshairs) { + mc.getTextureManager().bindTexture(Gui.icons); + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_ONE_MINUS_DST_COLOR, GL11.GL_ONE_MINUS_SRC_COLOR, GL11.GL_ONE, GL11.GL_ZERO); + GLStateManager.enableAlphaTest(); + drawTexturedModalRect(tessellator, (width >> 1) - 7, (height >> 1) - 7); + GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); + } + + GLStateManager.enableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + framebuffer.bindFramebufferTexture(); + drawTexturedRect(tessellator, (float) resolution.getScaledWidth_double(), (float) resolution.getScaledHeight_double()); + GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); + } + + if (framebuffer == null || dirty) { + dirty = false; + framebuffer = checkFramebufferSizes(framebuffer, mc.displayWidth, mc.displayHeight); + framebuffer.framebufferClear(); + framebuffer.bindFramebuffer(false); + GLStateManager.disableBlend(); + GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GLStateManager.disableLighting(); + GLStateManager.disableFog(); + renderingCacheOverride = true; + ingame.renderGameOverlay(partialTicks, b, i, j); + renderingCacheOverride = false; + mc.getFramebuffer().bindFramebuffer(false); + GLStateManager.enableBlend(); + } + + GLStateManager.enableDepthTest(); + } + + private static Framebuffer checkFramebufferSizes(Framebuffer framebuffer, int width, int height) { + if (framebuffer == null || framebuffer.framebufferWidth != width || framebuffer.framebufferHeight != height) { + if (framebuffer == null) { + framebuffer = new Framebuffer(width, height, true); + framebuffer.framebufferColor[0] = 0.0F; + framebuffer.framebufferColor[1] = 0.0F; + framebuffer.framebufferColor[2] = 0.0F; + } else { + framebuffer.createBindFramebuffer(width, height); + } + framebuffer.setFramebufferFilter(GL11.GL_NEAREST); + } + return framebuffer; + } + + private static void drawTexturedRect(Tessellator tessellator, float width, float height) { + GLStateManager.enableTexture(); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(0, height, 0.0, 0, 0); + tessellator.addVertexWithUV(width, height, 0.0, 1, 0); + tessellator.addVertexWithUV(width, 0, 0.0, 1, 1); + tessellator.addVertexWithUV(0, 0, 0.0, 0, 1); + tessellator.draw(); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + } + + private static void drawTexturedModalRect(Tessellator tessellator, int x, int y) { + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(x, y + 16, 100.0, 0.0, 0.0625); + tessellator.addVertexWithUV(x + 16, y + 16, 100.0, 0.0625, 0.0625); + tessellator.addVertexWithUV(x + 16, y, 100.0, 0.0625, 0.0); + tessellator.addVertexWithUV(x, y, 100.0, 0.0, 0.0); + tessellator.draw(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/hudcaching/LICENSE-HUDCaching.md b/src/main/java/com/gtnewhorizons/angelica/hudcaching/LICENSE-HUDCaching.md new file mode 100644 index 000000000..883eb6f6e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/hudcaching/LICENSE-HUDCaching.md @@ -0,0 +1,175 @@ +HUDCaching was backported from https://github.com/Sk1erLLC/Patcher under the following license: + +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + +Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +### Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). + +* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). + +## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +### Section 1 – Definitions. + +a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. + +d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. + +h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. + +i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. + +j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +### Section 2 – Scope. + +a. ___License grant.___ + +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and + + B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. + +2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + +3. __Term.__ The term of this Public License is specified in Section 6(a). + +4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. __Downstream recipients.__ + + A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + + C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. ___Other rights.___ + +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. + +### Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. ___Attribution.___ + +1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + +b. ___ShareAlike.___ + +In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. + +1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. + +2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. + +3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. + +### Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and + +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +### Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ + +b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ + +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +### Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + +2. upon express reinstatement by the Licensor. + +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +### Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +### Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. +> +> Creative Commons may be contacted at creativecommons.org diff --git a/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaLateMixins.java b/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaLateMixins.java deleted file mode 100644 index 3bb7f6545..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaLateMixins.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.gtnewhorizons.angelica.loading; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import com.gtnewhorizon.gtnhmixins.ILateMixinLoader; -import com.gtnewhorizon.gtnhmixins.LateMixin; -import com.gtnewhorizons.angelica.mixins.Mixins; - -@LateMixin -public class AngelicaLateMixins implements ILateMixinLoader { - - @Override - public String getMixinConfig() { - return "mixins.angelica.late.json"; - } - - @Override - public List getMixins(Set loadedMods) { - final List mixins = new ArrayList<>(); - final List notLoading = new ArrayList<>(); - for (Mixins mixin : Mixins.values()) { - if (mixin.phase == Mixins.Phase.LATE) { - if (mixin.shouldLoad(Collections.emptySet(), loadedMods)) { - mixins.addAll(mixin.mixinClasses); - } else { - notLoading.addAll(mixin.mixinClasses); - } - } - } - AngelicaTweaker.LOGGER.info("Not loading the following LATE mixins: {}", notLoading.toString()); - return mixins; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaTweaker.java b/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaTweaker.java index c15d47410..d247461ea 100644 --- a/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaTweaker.java +++ b/src/main/java/com/gtnewhorizons/angelica/loading/AngelicaTweaker.java @@ -1,33 +1,60 @@ package com.gtnewhorizons.angelica.loading; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.config.ConfigException; +import com.gtnewhorizons.angelica.config.ConfigurationManager; +import com.gtnewhorizons.angelica.mixins.Mixins; +import com.gtnewhorizons.angelica.mixins.TargetedMod; +import cpw.mods.fml.relauncher.FMLLaunchHandler; +import cpw.mods.fml.relauncher.IFMLLoadingPlugin; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; import org.spongepowered.asm.launch.GlobalProperties; import org.spongepowered.asm.service.mojang.MixinServiceLaunchWrapper; -import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader; -import com.gtnewhorizons.angelica.mixins.Mixins; - -import cpw.mods.fml.relauncher.IFMLLoadingPlugin; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; @IFMLLoadingPlugin.MCVersion("1.7.10") -@IFMLLoadingPlugin.SortingIndex(1100) +@IFMLLoadingPlugin.TransformerExclusions("com.gtnewhorizons.angelica.transform.RedirectorTransformer") +@IFMLLoadingPlugin.SortingIndex(Integer.MAX_VALUE - 5) public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader { - public static final Logger LOGGER = LogManager.getLogger("angelica"); + public static final Logger LOGGER = LogManager.getLogger("Angelica"); + + static { + try { + // Angelica Config + ConfigurationManager.registerConfig(AngelicaConfig.class); + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME); + loggerConfig.setLevel(Level.DEBUG); + ctx.updateLoggers(); + } catch (ConfigException e) { + throw new RuntimeException(e); + } + } @Override public String[] getASMTransformerClass() { - List tweakClasses = GlobalProperties.get(MixinServiceLaunchWrapper.BLACKBOARD_KEY_TWEAKCLASSES); - if (tweakClasses != null) { - tweakClasses.add(MixinCompatHackTweaker.class.getName()); + // Directly add this to the MixinServiceLaunchWrapper tweaker's list of Tweak Classes + List mixinTweakClasses = GlobalProperties.get(MixinServiceLaunchWrapper.BLACKBOARD_KEY_TWEAKCLASSES); + if (mixinTweakClasses != null) { + mixinTweakClasses.add(MixinCompatHackTweaker.class.getName()); } + + // Return any others here return null; } @@ -58,18 +85,49 @@ public String getMixinConfig() { @Override public List getMixins(Set loadedCoreMods) { + // TODO: Sodium +// mixins.addAll(getNotFineMixins(loadedCoreMods)); +// mixins.addAll(getArchaicMixins(loadedCoreMods)); + return Mixins.getEarlyMixins(loadedCoreMods); + } + + private List getNotFineMixins(Set loadedCoreMods) { + if(FMLLaunchHandler.side().isServer()) + return Collections.emptyList(); + final List mixins = new ArrayList<>(); - final List notLoading = new ArrayList<>(); - for (Mixins mixin : Mixins.values()) { - if (mixin.phase == Mixins.Phase.EARLY) { - if (mixin.shouldLoad(loadedCoreMods, Collections.emptySet())) { - mixins.addAll(mixin.mixinClasses); - } else { - notLoading.addAll(mixin.mixinClasses); - } - } - } - LOGGER.info("Not loading the following EARLY mixins: {}", notLoading.toString()); + mixins.add("notfine.clouds.MixinEntityRenderer"); + mixins.add("notfine.clouds.MixinGameSettings"); + mixins.add("notfine.clouds.MixinRenderGlobal"); + mixins.add("notfine.clouds.MixinWorldType"); + + mixins.add("notfine.leaves.MixinBlockLeaves"); + mixins.add("notfine.leaves.MixinBlockLeavesBase"); + + mixins.add("notfine.particles.MixinBlockEnchantmentTable"); + mixins.add("notfine.particles.MixinEffectRenderer"); + mixins.add("notfine.particles.MixinWorldClient"); + mixins.add("notfine.particles.MixinWorldProvider"); + + mixins.add("notfine.toggle.MixinGuiIngame"); + mixins.add("notfine.toggle.MixinEntityRenderer"); + mixins.add("notfine.toggle.MixinRender"); + mixins.add("notfine.toggle.MixinRenderItem"); return mixins; } + + private static final ImmutableMap MODS_BY_CLASS = ImmutableMap.builder() + .put("optifine.OptiFineForgeTweaker", TargetedMod.OPTIFINE) + .put("fastcraft.Tweaker", TargetedMod.FASTCRAFT) + .put("cofh.asm.LoadingPlugin", TargetedMod.COFHCORE) + .build(); + public static final Set coreMods = new HashSet<>(); + + private static void detectCoreMods(Set loadedCoreMods) { + MODS_BY_CLASS.forEach((key, value) -> { + if (loadedCoreMods.contains(key)) + coreMods.add(value); + }); + } + } diff --git a/src/main/java/com/gtnewhorizons/angelica/loading/MixinCompatHackTweaker.java b/src/main/java/com/gtnewhorizons/angelica/loading/MixinCompatHackTweaker.java index 494307e6a..2807bd169 100644 --- a/src/main/java/com/gtnewhorizons/angelica/loading/MixinCompatHackTweaker.java +++ b/src/main/java/com/gtnewhorizons/angelica/loading/MixinCompatHackTweaker.java @@ -1,36 +1,59 @@ package com.gtnewhorizons.angelica.loading; -import java.io.File; -import java.lang.reflect.Field; -import java.util.List; - +import com.gtnewhorizons.angelica.transform.RedirectorTransformer; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.relauncher.FMLLaunchHandler; import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.ITweaker; import net.minecraft.launchwrapper.Launch; import net.minecraft.launchwrapper.LaunchClassLoader; -import com.gtnewhorizons.angelica.transform.AClassTransformer; +import java.io.File; +import java.lang.reflect.Field; +import java.util.List; -import cpw.mods.fml.common.asm.transformers.TerminalTransformer; +import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER; public class MixinCompatHackTweaker implements ITweaker { - + public static final boolean DISABLE_OPTIFINE_AND_FASTCRAFT = true; @Override public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { - LaunchClassLoader lcl = Launch.classLoader; + if(DISABLE_OPTIFINE_AND_FASTCRAFT) { + LOGGER.info("Disabling Optifine and Fastcraft (if present)"); + disableOptifineAndFastcraft(); + } + } + + private void disableOptifineAndFastcraft() { + // Remove Optifine and Fastcraft transformers & Mod Containers try { + LaunchClassLoader lcl = Launch.classLoader; Field xformersField = lcl.getClass().getDeclaredField("transformers"); xformersField.setAccessible(true); @SuppressWarnings("unchecked") List xformers = (List) xformersField.get(lcl); - int terminalIndex; - for (terminalIndex = 1; terminalIndex < xformers.size(); terminalIndex++) { - if (xformers.get(terminalIndex) instanceof TerminalTransformer) { - break; + for (int idx = xformers.size() - 1; idx >= 0; idx--) { + final String name = xformers.get(idx).getClass().getName(); + if (name.startsWith("optifine") || name.startsWith("fastcraft")) { + LOGGER.info("Removing transformer " + name); + xformers.remove(idx); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + try { + Field injectedContainersField = Loader.class.getDeclaredField("injectedContainers"); + injectedContainersField.setAccessible(true); + @SuppressWarnings("unchecked") + List containers = (List ) injectedContainersField.get(Loader.class); + for (int idx = containers.size() - 1; idx >= 0; idx--) { + final String name = containers.get(idx); + if (name.startsWith("optifine") || name.startsWith("fastcraft")) { + LOGGER.info("Removing mod container " + name); + containers.remove(idx); } } - xformers.add(terminalIndex - 1, new AClassTransformer()); - AngelicaTweaker.LOGGER.info("Hacked in asm class transformer in position {}", terminalIndex - 1); } catch (Exception e) { throw new RuntimeException(e); } @@ -48,6 +71,11 @@ public String getLaunchTarget() { @Override public String[] getLaunchArguments() { + if (FMLLaunchHandler.side().isClient()) { + // Run after Mixins, but before LWJGl3ify + Launch.classLoader.registerTransformer(RedirectorTransformer.class.getName()); + } + return new String[0]; } } diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java index 6bdebf1fe..036801d03 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java @@ -1,114 +1,222 @@ package com.gtnewhorizons.angelica.mixins; +import com.gtnewhorizons.angelica.AngelicaMod; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import com.mitchej123.hodgepodge.Common; +import cpw.mods.fml.relauncher.FMLLaunchHandler; + import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.Supplier; -import cpw.mods.fml.relauncher.FMLLaunchHandler; - public enum Mixins { - - ANGELICA_STARTUP(new Builder("Start Angelica").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) - .setPhase(Phase.EARLY).addMixinClasses("shaders.MixinMinecraft")), - - ANGELICA_SHADERS_BUTTON(new Builder("Add Shaders Button").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) - .setPhase(Phase.EARLY).addMixinClasses("settings.MixinGuiVideoSettings")), - - ANGELICA_LIGHTING(new Builder("Lighting").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) - .setPhase(Phase.EARLY).addMixinClasses("lighting.MixinBlock", "lighting.MixinRenderBlocks")), - - ANGELICA_RENDERER(new Builder("Renderer").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) - .setPhase(Phase.EARLY).addMixinClasses( - "renderer.MixinAbstractTexture", - "renderer.MixinDynamicTexture", - "renderer.MixinEntityRenderer", - "renderer.MixinItemRenderer", - "renderer.MixinITextureObject", - "renderer.MixinLayeredTexture", - "renderer.MixinModelRenderer", - "renderer.MixinOpenGlHelper", - "renderer.MixinRender", - "renderer.MixinRenderBlocks", - "renderer.MixinRenderDragon", - "renderer.MixinRenderEnderman", - "renderer.MixinRendererLivingEntity", - "renderer.MixinRenderGlobal", - "renderer.MixinRenderSpider", - "renderer.MixinSimpleTexture", - "renderer.MixinTessellator", - "renderer.MixinTextureAtlasSprite", - "renderer.MixinTextureClock", - "renderer.MixinTextureCompass", - "renderer.MixinTextureManager", - "renderer.MixinTextureMap", - "renderer.MixinThreadDownloadImageData")), - - COMPAT_PR_ILLUMINATION( - new Builder("ProjectRed Illumination compat").addTargetedMod(TargetedMod.PROJECTRED_ILLUMINATION) - .setSide(Side.CLIENT).addMixinClasses("compat.MixinRenderHalo")), - - COMPAT_SMART_RENDER(new Builder("Smart Render compat").addTargetedMod(TargetedMod.SMART_RENDER).setSide(Side.CLIENT) - .addMixinClasses("compat.MixinModelRotationRenderer")) - + ANGELICA_STARTUP(new Builder("Angelica Startup").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).addMixinClasses( + "angelica.startup.MixinInitGLStateManager" + ,"angelica.startup.MixinSplashProgress" + ) + ), + + ANGELICA(new Builder("Angelica").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).addMixinClasses( + "angelica.MixinEntityRenderer" + + ) + ), + + ANGELICA_FONT_RENDERER(new Builder("Angelica Font Renderer").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableFontRenderer).addMixinClasses( + "angelica.fontrenderer.MixinGuiIngameForge" + ,"angelica.fontrenderer.MixinFontRenderer" + ) + ), + + ANGELICA_ENABLE_DEBUG(new Builder("Angelica Debug").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaMod.lwjglDebug).addMixinClasses( + "angelica.debug.MixinSplashProgress" + ) + ), + // Not compatible with the lwjgl debug callbacks, so disable if that's enabled + ARCHAIC_SPLASH(new Builder("ArchaicFix Splash").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.showSplashMemoryBar && !AngelicaMod.lwjglDebug).addMixinClasses( + "angelica.archaic.MixinSplashProgress" + ,"angelica.archaic.AccessorSplashProgress" + ) + ), + + ARCHAIC_CORE(new Builder("Archaic Core").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).addMixinClasses( + "angelica.archaic.MixinBlockFence" + ,"angelica.archaic.MixinFMLClientHandler" + ,"angelica.archaic.MixinGuiIngameForge" + ,"angelica.archaic.MixinNetHandlerPlayClient" + ,"angelica.archaic.MixinThreadDownloadImageData" + ) + ), + + IRIS_STARTUP(new Builder("Start Iris").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableIris).addMixinClasses( + "shaders.startup.MixinGameSettings" + ,"shaders.startup.MixinGuiMainMenu" + ,"shaders.startup.MixinInitRenderer" + ,"shaders.startup.MixinAbstractTexture" + ,"shaders.startup.MixinTextureAtlasSprite" + ) + ), + + SODIUM_STARTUP(new Builder("Start Sodium").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableSodium && !AngelicaConfig.enableIris).addMixinClasses( + "sodium.startup.MixinInitDebug" + ) + ), + + SODIUM(new Builder("Sodium").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableSodium).addMixinClasses( + "sodium.MixinChunkProviderClient" + ,"sodium.MixinBlock" + ,"sodium.MixinChunk" + ,"sodium.MixinChunkProviderServer" + ,"sodium.MixinEntity" + ,"sodium.MixinRenderManager" + ,"sodium.MixinExtendedBlockStorage" + ,"sodium.MixinEntityRenderer" + ,"sodium.MixinFMLClientHandler" + ,"sodium.MixinGameSettings" + ,"sodium.MixinFrustrum" + ,"sodium.MixinMaterial" + ,"sodium.MixinMinecraft" + ,"sodium.MixinNibbleArray" + ,"sodium.MixinRenderBlocks" + ,"sodium.MixinRenderGlobal" + ,"sodium.MixinWorldClient" + ,"sodium.MixinTessellator" + ,"sodium.MixinGuiIngameForge" + ,"sodium.MixinEffectRenderer" + // TODO Doesn't work correctly + //,"sodium.MixinTextureAtlasSprite" + //,"sodium.MixinTextureMap" + //,"sodium.MixinEntityFX" + ,"sodium.MixinLongHashMap" + ,"sodium.MixinRender" + ) + ), + + // Required for Sodium's FluidRenderer, so it treats vanilla liquids as IFluidBlocks + SODIUM_WISHLIST(new Builder("Sodiumer").addTargetedMod(TargetedMod.VANILLA).setSide(Side.BOTH) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableSodiumFluidRendering).addMixinClasses( + "sodium.MixinBlockLiquid")), + + IRIS_RENDERING(new Builder("Iris Shaders").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableIris).addMixinClasses( + "shaders.MixinEntityRenderer" + ,"shaders.MixinFramebuffer" + ,"shaders.MixinItem" + ,"shaders.MixinLocale" + ,"shaders.MixinOpenGlHelper" + ,"shaders.MixinRender" + ,"shaders.MixinRenderGlobal" + ) + ), + + IRIS_ACCESSORS(new Builder("Iris Accessors").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableIris).addMixinClasses( + "shaders.accessors.MinecraftAccessor" + ,"shaders.accessors.EntityRendererAccessor" + ,"shaders.accessors.SimpleTextureAccessor" + ,"shaders.accessors.TextureAtlasSpriteAccessor" + ,"shaders.accessors.TextureMapAccessor" + ,"shaders.accessors.AnimationMetadataSectionAccessor" + ) + ), + + + ANGELICA_TEXTURE(new Builder("Textures").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableIris || AngelicaConfig.enableSodium).addMixinClasses( + "angelica.textures.MixinTextureAtlasSprite" + ,"angelica.textures.MixinTextureUtil" + )), + + HUD_CACHING(new Builder("Renders the HUD elements 20 times per second maximum to improve performance") + .addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT).setPhase(Phase.EARLY) + .setApplyIf(() -> AngelicaConfig.enableHudCaching).addMixinClasses( + "angelica.hudcaching.GuiIngameForgeAccessor", + "angelica.hudcaching.MixinEntityRenderer_HUDCaching", + "angelica.hudcaching.MixinFramebuffer_HUDCaching", + "angelica.hudcaching.MixinGuiIngame_HUDCaching", + "angelica.hudcaching.MixinGuiIngameForge_HUDCaching", + "angelica.hudcaching.MixinRenderItem") + + ), + + OPTIMIZE_WORLD_UPDATE_LIGHT(new Builder("Optimize world updateLightByType method").setPhase(Phase.EARLY) + .setSide(Side.BOTH).addTargetedMod(TargetedMod.VANILLA).setApplyIf(() -> AngelicaConfig.optimizeWorldUpdateLight) + .addMixinClasses("angelica.lighting.MixinWorld_FixLightUpdateLag")), + + + SPEEDUP_VANILLA_ANIMATIONS(new Builder("Speedup Vanilla Animations").setPhase(Phase.EARLY) + .setApplyIf(() -> AngelicaConfig.speedupAnimations).setSide(Side.CLIENT).addTargetedMod(TargetedMod.VANILLA) + .addMixinClasses( + "angelica.animation.MixinTextureAtlasSprite", + "angelica.animation.MixinTextureMap", + "angelica.animation.MixinBlockFire", + "angelica.animation.MixinMinecraftForgeClient", + "angelica.animation.MixinChunkCache", + "angelica.animation.MixinRenderBlocks", + "angelica.animation.MixinRenderBlockFluid", + "angelica.animation.MixinWorldRenderer", + "angelica.animation.MixinRenderItem")), + + OPTIMIZE_TEXTURE_LOADING(new Builder("Optimize Texture Loading").setPhase(Phase.EARLY) + .addMixinClasses("angelica.textures.MixinTextureUtil_OptimizeMipmap").addTargetedMod(TargetedMod.VANILLA) + .setApplyIf(() -> AngelicaConfig.optimizeTextureLoading).setSide(Side.CLIENT)), + + NOTFINE_OPTIMIZATION(new Builder("NotFine Optimizations").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableNotFineOptimizations).addMixinClasses( + "notfine.faceculling.MixinBlock" + ,"notfine.faceculling.MixinBlockSlab" + ,"notfine.faceculling.MixinBlockSnow" + ,"notfine.faceculling.MixinBlockStairs" + )), + + NOTFINE_FEATURES(new Builder("NotFine Features").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) + .setPhase(Phase.EARLY).setApplyIf(() -> AngelicaConfig.enableNotFineFeatures).addMixinClasses( + "notfine.glint.MixinItemRenderer" + ,"notfine.glint.MixinRenderBiped" + ,"notfine.glint.MixinRenderItem" + ,"notfine.glint.MixinRenderPlayer" + ,"notfine.gui.MixinGuiSlot" + ,"notfine.renderer.MixinRenderGlobal" + ,"notfine.settings.MixinGameSettings" + )), + NOTFINE_LATE_TWILIGHT_FORESTLEAVES(new Builder("NotFine Mod Leaves").addTargetedMod(TargetedMod.TWILIGHT_FOREST).setSide(Side.CLIENT) + .setPhase(Phase.LATE).setApplyIf(() -> AngelicaConfig.enableNotFineFeatures).addMixinClasses( + "notfine.leaves.twilightforest.MixinBlockTFLeaves" + ,"notfine.leaves.twilightforest.MixinBlockTFLeaves3" + // TODO: Verify 2.3.8.18 or later to support non NH builds? + ,"notfine.leaves.twilightforest.MixinBlockTFMagicLeaves" + )), + NOTFINE_LATE_THAUMCRAFT_LEAVES(new Builder("NotFine Mod Leaves").addTargetedMod(TargetedMod.THAUMCRAFT).setSide(Side.CLIENT) + .setPhase(Phase.LATE).setApplyIf(() -> AngelicaConfig.enableNotFineFeatures).addMixinClasses( + "notfine.leaves.thaumcraft.MixinBlockMagicalLeaves" + )), + NOTFINE_LATE_WITCHERY_LEAVES(new Builder("NotFine Mod Leaves").addTargetedMod(TargetedMod.WITCHERY).setSide(Side.CLIENT) + .setPhase(Phase.LATE).setApplyIf(() -> AngelicaConfig.enableNotFineFeatures).addMixinClasses( + "notfine.leaves.witchery.MixinBlockWitchLeaves" + )) ; - public final String name; - public final List mixinClasses; + private final List mixinClasses; private final Supplier applyIf; - public final Phase phase; + private final Phase phase; private final Side side; - public final List targetedMods; - public final List excludedMods; - - private static class Builder { - - private final String name; - private final List mixinClasses = new ArrayList<>(); - private Supplier applyIf = () -> true; - private Side side = Side.BOTH; - private Phase phase = Phase.LATE; - private final List targetedMods = new ArrayList<>(); - private final List excludedMods = new ArrayList<>(); - - public Builder(String name) { - this.name = name; - } - - public Builder addMixinClasses(String... mixinClasses) { - this.mixinClasses.addAll(Arrays.asList(mixinClasses)); - return this; - } - - public Builder setPhase(Phase phase) { - this.phase = phase; - return this; - } - - public Builder setSide(Side side) { - this.side = side; - return this; - } - - public Builder setApplyIf(Supplier applyIf) { - this.applyIf = applyIf; - return this; - } - - public Builder addTargetedMod(TargetedMod mod) { - this.targetedMods.add(mod); - return this; - } - - public Builder addExcludedMod(TargetedMod mod) { - this.excludedMods.add(mod); - return this; - } - } + private final List targetedMods; + private final List excludedMods; Mixins(Builder builder) { - this.name = builder.name; this.mixinClasses = builder.mixinClasses; this.applyIf = builder.applyIf; this.side = builder.side; @@ -116,16 +224,48 @@ public Builder addExcludedMod(TargetedMod mod) { this.excludedMods = builder.excludedMods; this.phase = builder.phase; if (this.targetedMods.isEmpty()) { - throw new RuntimeException("No targeted mods specified for " + this.name); + throw new RuntimeException("No targeted mods specified for " + this.name()); } if (this.applyIf == null) { - throw new RuntimeException("No ApplyIf function specified for " + this.name); + throw new RuntimeException("No ApplyIf function specified for " + this.name()); } } + public static List getEarlyMixins(Set loadedCoreMods) { + final List mixins = new ArrayList<>(); + final List notLoading = new ArrayList<>(); + for (Mixins mixin : Mixins.values()) { + if (mixin.phase == Mixins.Phase.EARLY) { + if (mixin.shouldLoad(loadedCoreMods, Collections.emptySet())) { + mixins.addAll(mixin.mixinClasses); + } else { + notLoading.addAll(mixin.mixinClasses); + } + } + } + AngelicaTweaker.LOGGER.info("Not loading the following EARLY mixins: {}", notLoading); + return mixins; + } + + public static List getLateMixins(Set loadedMods) { + final List mixins = new ArrayList<>(); + final List notLoading = new ArrayList<>(); + for (Mixins mixin : Mixins.values()) { + if (mixin.phase == Mixins.Phase.LATE) { + if (mixin.shouldLoad(Collections.emptySet(), loadedMods)) { + mixins.addAll(mixin.mixinClasses); + } else { + notLoading.addAll(mixin.mixinClasses); + } + } + } + AngelicaTweaker.LOGGER.info("Not loading the following LATE mixins: {}", notLoading.toString()); + return mixins; + } + private boolean shouldLoadSide() { - return (side == Side.BOTH || (side == Side.SERVER && FMLLaunchHandler.side().isServer()) - || (side == Side.CLIENT && FMLLaunchHandler.side().isClient())); + return side == Side.BOTH || (side == Side.SERVER && FMLLaunchHandler.side().isServer()) + || (side == Side.CLIENT && FMLLaunchHandler.side().isClient()); } private boolean allModsLoaded(List targetedMods, Set loadedCoreMods, Set loadedMods) { @@ -160,19 +300,61 @@ private boolean noModsLoaded(List targetedMods, Set loadedC return true; } - public boolean shouldLoad(Set loadedCoreMods, Set loadedMods) { + private boolean shouldLoad(Set loadedCoreMods, Set loadedMods) { return (shouldLoadSide() && applyIf.get() && allModsLoaded(targetedMods, loadedCoreMods, loadedMods) && noModsLoaded(excludedMods, loadedCoreMods, loadedMods)); } - enum Side { + private static class Builder { + + private final List mixinClasses = new ArrayList<>(); + private Supplier applyIf = () -> true; + private Side side = Side.BOTH; + private Phase phase = Phase.LATE; + private final List targetedMods = new ArrayList<>(); + private final List excludedMods = new ArrayList<>(); + + public Builder(@SuppressWarnings("unused") String description) {} + + public Builder addMixinClasses(String... mixinClasses) { + this.mixinClasses.addAll(Arrays.asList(mixinClasses)); + return this; + } + + public Builder setPhase(Phase phase) { + this.phase = phase; + return this; + } + + public Builder setSide(Side side) { + this.side = side; + return this; + } + + public Builder setApplyIf(Supplier applyIf) { + this.applyIf = applyIf; + return this; + } + + public Builder addTargetedMod(TargetedMod mod) { + this.targetedMods.add(mod); + return this; + } + + public Builder addExcludedMod(TargetedMod mod) { + this.excludedMods.add(mod); + return this; + } + } + + private enum Side { BOTH, CLIENT, SERVER } - public enum Phase { + private enum Phase { EARLY, LATE, } diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/TargetedMod.java b/src/main/java/com/gtnewhorizons/angelica/mixins/TargetedMod.java index 15df372a1..a573a5e88 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/TargetedMod.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/TargetedMod.java @@ -1,20 +1,37 @@ package com.gtnewhorizons.angelica.mixins; +import lombok.Getter; + public enum TargetedMod { - VANILLA("Minecraft", null), - FASTCRAFT("FastCraft", "fastcraft.Tweaker"), - OPTIFINE("Optifine", "optifine.OptiFineForgeTweaker", "Optifine"), - GTNHLIB("GTNHLib", "com.gtnewhorizon.gtnhlib.core.GTNHLibCore", "gtnhlib"), - LWJGL3IFY("lwjgl3ify", "me.eigenraven.lwjgl3ify.core.Lwjgl3ifyCoremod", "lwjgl3ify"), - PROJECTRED_ILLUMINATION("ProjectRed Illumination", null, "ProjRed|Illumination"), - SMART_RENDER("Smart Render", null, "SmartRender"); + VANILLA("Minecraft", null) + , FASTCRAFT("FastCraft", "fastcraft.Tweaker") + , OPTIFINE("Optifine", "optifine.OptiFineForgeTweaker", "Optifine") + , BOTANIA("Botania", null, "Botania") + , CHICKENCHUNKS("ChickenChunks", null, "ChickenChunks") + , COFHCORE("CoFHCore", "cofh.asm.LoadingPlugin", "CoFHCore") + , EXTRAUTILS("ExtraUtilities", null, "ExtraUtilities") + , GTNHLIB("GTNHLib", "com.gtnewhorizon.gtnhlib.core.GTNHLibCore", "gtnhlib") + , JABBA("JABBA", null, "JABBA") + , JOURNEYMAP("JourneyMap", null, "journeymap") + , LWJGL3IFY("lwjgl3ify", "me.eigenraven.lwjgl3ify.core.Lwjgl3ifyCoremod", "lwjgl3ify") + , MRTJPCORE("MrTJPCore", null, "MrTJPCoreMod") + , PROJECTRED_ILLUMINATION("ProjectRed Illumination", null, "ProjRed|Illumination") + , SMART_RENDER("Smart Render", null, "SmartRender") + , THAUMCRAFT("Thaumcraft", null, "Thaumcraft") + , TWILIGHT_FOREST("TwilightForest", null, "TwilightForest") + , WITCHERY("Witchery", null, "witchery") + ; + /** The "name" in the @Mod annotation */ + @Getter public final String modName; /** Class that implements the IFMLLoadingPlugin interface */ + @Getter public final String coreModClass; /** The "modid" in the @Mod annotation */ + @Getter public final String modId; TargetedMod(String modName, String coreModClass) { diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/dummy/DummyTessellator.java b/src/main/java/com/gtnewhorizons/angelica/mixins/dummy/DummyTessellator.java new file mode 100644 index 000000000..a4017740f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/dummy/DummyTessellator.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.dummy; + +import net.minecraft.client.renderer.Tessellator; + +/** + * No-op tessellator to replace the inconvienent one + */ +public class DummyTessellator extends Tessellator { + + public static Tessellator instance = new DummyTessellator(); + + @Override + public void startDrawingQuads() {} + + @Override + public void setColorOpaque_I(int whocares) {} + + @Override + public void addVertex(double thing1, double thing2, double thereaintnothing3) {} + + @Override + public int draw() { + return 0; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinEntityRenderer.java new file mode 100644 index 000000000..39b918ef7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinEntityRenderer.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica; + +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.entity.EntityLivingBase; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityRenderer.class) +public abstract class MixinEntityRenderer { + @Inject(method = "setupCameraTransform", at = @At(value = "TAIL")) + private void angelica$captureCameraMatrix(float partialTicks, int startTime, CallbackInfo ci) { + final Minecraft mc = Minecraft.getMinecraft(); + final EntityLivingBase viewEntity = mc.renderViewEntity; + + RenderingState.INSTANCE.setCameraPosition( + viewEntity.lastTickPosX + (viewEntity.posX - viewEntity.lastTickPosX) * partialTicks, + viewEntity.lastTickPosY + (viewEntity.posY - viewEntity.lastTickPosY) * partialTicks, + viewEntity.lastTickPosZ + (viewEntity.posZ - viewEntity.lastTickPosZ) * partialTicks + ); + RenderingState.INSTANCE.captureProjectionMatrix(); + RenderingState.INSTANCE.captureModelViewMatrix(); + + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinBlockFire.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinBlockFire.java new file mode 100644 index 000000000..47178e159 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinBlockFire.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils; +import net.minecraft.block.BlockFire; +import net.minecraft.util.IIcon; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockFire.class) +public class MixinBlockFire { + + @Shadow + private IIcon[] field_149850_M; + + @Inject(method = "getFireIcon", at = @At("HEAD")) + private void hodgepodge$markFireAnimationForUpdate(int p_149840_1_, CallbackInfoReturnable cir) { + AnimationsRenderUtils.markBlockTextureForUpdate(field_149850_M[p_149840_1_]); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinChunkCache.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinChunkCache.java new file mode 100644 index 000000000..e8a7f2dee --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinChunkCache.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.mixins.interfaces.ITexturesCache; +import net.minecraft.util.IIcon; +import net.minecraft.world.ChunkCache; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.HashSet; + +@Mixin(ChunkCache.class) +public class MixinChunkCache implements ITexturesCache { + + @Unique + private final HashSet renderedIcons = new HashSet<>(); + + @Override + public HashSet getRenderedTextures() { + return renderedIcons; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinMinecraftForgeClient.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinMinecraftForgeClient.java new file mode 100644 index 000000000..8b6d7152b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinMinecraftForgeClient.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.IIcon; +import net.minecraftforge.client.IItemRenderer; +import net.minecraftforge.client.MinecraftForgeClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MinecraftForgeClient.class) +public class MixinMinecraftForgeClient { + + /** + * @author laetansky We can just mark any item texture that gets rendered for an update + */ + @Inject(method = "getItemRenderer", at = @At("HEAD"), remap = false) + private static void hodgepodge$beforeRenderItem(ItemStack itemStack, IItemRenderer.ItemRenderType type, + CallbackInfoReturnable cir) { + final Item item = itemStack.getItem(); + if (item.requiresMultipleRenderPasses()) { + for (int i = 0; i < item.getRenderPasses(itemStack.getItemDamage()); i++) { + IIcon icon = item.getIcon(itemStack, i); + if (icon instanceof TextureAtlasSprite) { + ((IPatchedTextureAtlasSprite) icon).markNeedsAnimationUpdate(); + } + } + } else { + final IIcon icon = itemStack.getIconIndex(); + if (icon instanceof TextureAtlasSprite) { + ((IPatchedTextureAtlasSprite) icon).markNeedsAnimationUpdate(); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlockFluid.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlockFluid.java new file mode 100644 index 000000000..de4a96698 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlockFluid.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.fluids.RenderBlockFluid; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = RenderBlockFluid.class, remap = false) +public abstract class MixinRenderBlockFluid { + + @Unique + private IBlockAccess currentBlockAccess; + + @Inject(method = "renderWorldBlock", at = @At(value = "HEAD")) + private void hodgepodge$saveCurrentBlockAccess(IBlockAccess world, int x, int y, int z, Block block, int modelId, + RenderBlocks renderer, CallbackInfoReturnable cir) { + currentBlockAccess = world; + } + + /** + * @author laetansky + * @reason mark texture for update + */ + @Overwrite + private IIcon getIcon(IIcon icon) { + if (icon != null) { + AnimationsRenderUtils.markBlockTextureForUpdate(icon, currentBlockAccess); + return icon; + } + return ((TextureMap) Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)) + .getAtlasSprite("missingno"); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlocks.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlocks.java new file mode 100644 index 000000000..ffdc7c827 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderBlocks.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils; +import net.minecraft.block.Block; +import net.minecraft.block.BlockFire; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RenderBlocks.class) +public class MixinRenderBlocks { + + @Shadow + public IBlockAccess blockAccess; + + @Shadow + public IIcon overrideBlockTexture; + + /** + * @author laetansky Here where things get very tricky. We can't just mark blocks textures for update because this + * method gets called only when chunk render cache needs an update (that happens when a state of any block + * in that chunk changes). What we can do though is pass the rendered textures up to the WorldRenderer and + * later use it in RenderGlobal to mark textures for update and before that even sort WorldRenderers and + * apply Occlusion Querry (Basically that means that we will only mark those textures for update that are + * visible (on the viewport) at the moment) + */ + @Inject(method = "*(Lnet/minecraft/block/Block;DDDLnet/minecraft/util/IIcon;)V", at = @At("HEAD")) + public void hodgepodge$beforeRenderFace(Block p_147761_1_, double p_147761_2_, double p_147761_4_, + double p_147761_6_, IIcon icon, CallbackInfo ci) { + if (overrideBlockTexture != null) { + icon = overrideBlockTexture; + } + + AnimationsRenderUtils.markBlockTextureForUpdate(icon, blockAccess); + } + + @Inject(method = "renderBlockFire", at = @At("HEAD")) + public void hodgepodge$markFireBlockAnimationForUpdate(BlockFire instance, int x, int y, int z, + CallbackInfoReturnable cir) { + AnimationsRenderUtils.markBlockTextureForUpdate(instance.getFireIcon(0), blockAccess); + AnimationsRenderUtils.markBlockTextureForUpdate(instance.getFireIcon(1), blockAccess); + } + + @ModifyVariable(method = "renderBlockLiquid", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/renderer/RenderBlocks;getBlockIconFromSideAndMetadata(Lnet/minecraft/block/Block;II)Lnet/minecraft/util/IIcon;")) + public IIcon hodgepodge$markFluidAnimationForUpdate(IIcon icon) { + AnimationsRenderUtils.markBlockTextureForUpdate(icon, blockAccess); + + return icon; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderItem.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderItem.java new file mode 100644 index 000000000..00c88c523 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinRenderItem.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.util.IIcon; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderItem.class) +public class MixinRenderItem { + + /** + * Some mods may call it to render their internally stored icons, so we make sure we mark those for an update + */ + @Inject(method = "renderIcon", at = @At("HEAD")) + private void hodgepodge$beforeRenderIcon(int p_94149_1_, int p_94149_2_, IIcon icon, int p_94149_4_, int p_94149_5_, + CallbackInfo ci) { + if (icon instanceof TextureAtlasSprite) { + ((IPatchedTextureAtlasSprite) icon).markNeedsAnimationUpdate(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java new file mode 100644 index 000000000..eed3a916c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.List; + +@Mixin(TextureAtlasSprite.class) +public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSprite { + + @Unique + private boolean needsAnimationUpdate = false; + + @Shadow + protected int tickCounter; + @Shadow + protected int frameCounter; + + @Shadow + private AnimationMetadataSection animationMetadata; + + @Shadow + protected List framesTextureData; + + @Override + public void markNeedsAnimationUpdate() { + needsAnimationUpdate = true; + } + + @Override + public boolean needsAnimationUpdate() { + return needsAnimationUpdate; + } + + @Override + public void unmarkNeedsAnimationUpdate() { + needsAnimationUpdate = false; + } + + @Override + public void updateAnimationsDryRun() { + // account for weird subclass that doesn't use the stock mechanisms for animation + if (animationMetadata == null || framesTextureData == null) return; + + tickCounter++; + if (tickCounter >= animationMetadata.getFrameTimeSingle(frameCounter)) { + int j = this.animationMetadata.getFrameCount() == 0 ? framesTextureData.size() + : this.animationMetadata.getFrameCount(); + this.frameCounter = (this.frameCounter + 1) % j; + this.tickCounter = 0; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java new file mode 100644 index 000000000..e75b58bc1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java @@ -0,0 +1,60 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.AngelicaMod; +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import com.gtnewhorizons.angelica.utils.AnimationMode; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.List; + +@Mixin(value = TextureMap.class, priority = 999) +@SuppressWarnings("ForLoopReplaceableByForEach") +public abstract class MixinTextureMap extends AbstractTexture { + + @Shadow + @Final + private List listAnimatedSprites; + + @Unique + private static final Minecraft mc = Minecraft.getMinecraft(); + + /** + * @author laetansky + * @reason only update animations for textures that are being currently drawn By default minecraft handles any + * animations that present in listAnimatedSprites no matter if you see it or not which can lead to a huge + * performance decrease + */ + @Overwrite + public void updateAnimations() { + final boolean renderAllAnimations = AngelicaMod.animationsMode.is(AnimationMode.ALL); + final boolean renderVisibleAnimations = AngelicaMod.animationsMode.is(AnimationMode.VISIBLE_ONLY); + + mc.mcProfiler.startSection("updateAnimations"); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, this.getGlTextureId()); + // C Style loop should be faster + final int size = listAnimatedSprites.size(); + for (int i = 0; i < size; i++) { + final TextureAtlasSprite textureAtlasSprite = listAnimatedSprites.get(i); + final IPatchedTextureAtlasSprite patchedTextureAtlasSprite = ((IPatchedTextureAtlasSprite) textureAtlasSprite); + + if (renderAllAnimations || (renderVisibleAnimations && patchedTextureAtlasSprite.needsAnimationUpdate())) { + mc.mcProfiler.startSection(textureAtlasSprite.getIconName()); + textureAtlasSprite.updateAnimation(); + patchedTextureAtlasSprite.unmarkNeedsAnimationUpdate(); + mc.mcProfiler.endSection(); + } else { + patchedTextureAtlasSprite.updateAnimationsDryRun(); + } + } + mc.mcProfiler.endSection(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinWorldRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinWorldRenderer.java new file mode 100644 index 000000000..01da53aa0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinWorldRenderer.java @@ -0,0 +1,46 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.animation; + +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import com.gtnewhorizons.angelica.mixins.interfaces.ITexturesCache; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Set; + +@Mixin(WorldRenderer.class) +public class MixinWorldRenderer implements ITexturesCache { + + @Shadow + public boolean isInFrustum; + + @Unique + private Set renderedIcons; + + @ModifyArg(method = "updateRenderer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderBlocks;(Lnet/minecraft/world/IBlockAccess;)V")) + private IBlockAccess hodgepodge$onUpdateRenderer(IBlockAccess chunkCache) { + renderedIcons = ((ITexturesCache) chunkCache).getRenderedTextures(); + return chunkCache; + } + + @Inject(method = "getGLCallListForPass", at = @At("HEAD")) + private void hodgepodge$getGLCallListForPass(int pass, CallbackInfoReturnable cir) { + if (isInFrustum && pass == 0 && renderedIcons != null) { + for (IIcon icon : renderedIcons) { + ((IPatchedTextureAtlasSprite) icon).markNeedsAnimationUpdate(); + } + } + } + + @Override + public Set getRenderedTextures() { + return renderedIcons; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/AccessorSplashProgress.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/AccessorSplashProgress.java new file mode 100644 index 000000000..0d7459ea7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/AccessorSplashProgress.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import cpw.mods.fml.client.SplashProgress; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@SuppressWarnings("deprecation") +@Mixin(SplashProgress.class) +public interface AccessorSplashProgress { + + @Accessor(remap = false) + static int getBarBorderColor() { + throw new AssertionError(); + } + @Accessor(remap = false) + static int getBarBackgroundColor() { + throw new AssertionError(); + } + @Accessor(remap = false) + static int getFontColor() { + throw new AssertionError(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinBlockFence.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinBlockFence.java new file mode 100644 index 000000000..c4e04b06d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinBlockFence.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockFence; +import net.minecraft.block.material.Material; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockFence.class) +public abstract class MixinBlockFence extends Block { + private MixinBlockFence(Material p_i45394_1_) { + super(p_i45394_1_); + } + + /** + * Fix a smooth lighting glitch with fences against solid blocks. + */ + @Inject(method = "", at = @At("RETURN")) + private void makeTransparent(String s, Material m, CallbackInfo ci) { + this.canBlockGrass = true; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinFMLClientHandler.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinFMLClientHandler.java new file mode 100644 index 000000000..4d410af81 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinFMLClientHandler.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import cpw.mods.fml.client.FMLClientHandler; +import cpw.mods.fml.client.GuiModList; +import net.minecraft.client.gui.GuiIngameMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FMLClientHandler.class) +public abstract class MixinFMLClientHandler { + @Shadow(remap = false) public abstract void showGuiScreen(Object clientGuiElement); + + @Inject(method = "showInGameModOptions", at = @At("HEAD"), cancellable = true, remap = false) + private void showModsList(GuiIngameMenu guiIngameMenu, CallbackInfo ci) { + showGuiScreen(new GuiModList(guiIngameMenu)); + ci.cancel(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinGuiIngameForge.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinGuiIngameForge.java new file mode 100644 index 000000000..e9fe26039 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinGuiIngameForge.java @@ -0,0 +1,20 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import cpw.mods.fml.common.ModContainer; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraftforge.client.GuiIngameForge; +import com.gtnewhorizons.angelica.helpers.LoadControllerHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(GuiIngameForge.class) +public class MixinGuiIngameForge { + @Redirect(method = "renderHUDText", at = @At(value = "FIELD", target = "Lnet/minecraft/world/biome/BiomeGenBase;biomeName:Ljava/lang/String;")) + private String getModNameWithBiome(BiomeGenBase biome) { + ModContainer theMod = LoadControllerHelper.getOwningMod(biome.getClass()); + if(theMod == null) + return biome.biomeName; + return biome.biomeName + " [" + theMod.getName() + "]"; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinNetHandlerPlayClient.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinNetHandlerPlayClient.java new file mode 100644 index 000000000..c4db1f4b4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinNetHandlerPlayClient.java @@ -0,0 +1,31 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.play.INetHandlerPlayClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Makes interdimensional teleportation nearly as fast as same-dimension + * teleportation by removing the "Downloading terrain..." screen. This will cause + * the player to see partially loaded terrain rather than waiting for the whole + * drawScreen distance to load, but that's also the vanilla behaviour for same-dimension + * teleportation. [From ArchaicFix] + */ +@Mixin(value = NetHandlerPlayClient.class, priority = 500) +public abstract class MixinNetHandlerPlayClient implements INetHandlerPlayClient { + + @Redirect(method = "handleJoinGame", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;displayGuiScreen(Lnet/minecraft/client/gui/GuiScreen;)V")) + private void onGuiDisplayJoin(Minecraft mc, GuiScreen guiScreenIn) { + mc.displayGuiScreen(AngelicaConfig.hideDownloadingTerrainScreen ? null : guiScreenIn); + } + + @Redirect(method = "handleRespawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;displayGuiScreen(Lnet/minecraft/client/gui/GuiScreen;)V")) + private void onGuiDisplayRespawn(Minecraft mc, GuiScreen guiScreenIn) { + mc.displayGuiScreen(AngelicaConfig.hideDownloadingTerrainScreen ? null : guiScreenIn); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinSplashProgress.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinSplashProgress.java new file mode 100644 index 000000000..7b2e2a74a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinSplashProgress.java @@ -0,0 +1,143 @@ +/* + * Based off SplashProgress from Forge 14.23.5.2860. + */ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import com.gtnewhorizons.angelica.Tags; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import cpw.mods.fml.client.SplashProgress; +import net.minecraft.client.gui.FontRenderer; +import org.apache.commons.lang3.StringUtils; +import org.lwjgl.opengl.Display; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.lang.reflect.Field; + +import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; +import static org.lwjgl.opengl.GL11.glDisable; +import static org.lwjgl.opengl.GL11.glEnable; +import static org.lwjgl.opengl.GL11.glPopMatrix; +import static org.lwjgl.opengl.GL11.glPushMatrix; +import static org.lwjgl.opengl.GL11.glScalef; +import static org.lwjgl.opengl.GL11.glTranslatef; + +@SuppressWarnings("deprecation") +@Mixin(targets = { "cpw/mods/fml/client/SplashProgress$3" }) +public class MixinSplashProgress { + private static final int memoryGoodColor = 0x78CB34; + private static final int memoryWarnColor = 0xE6E84A; + private static final int memoryLowColor = 0xE42F2F; + private static float memoryColorPercent; + private static long memoryColorChangeTime; + private static FontRenderer fontRenderer = null; + @Inject(method = "run", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", ordinal = 1, remap = false, shift = At.Shift.AFTER), remap = false, require = 0) + private void injectDrawMemoryBar(CallbackInfo ci) { + // NOTE: Disable this if you're checking for off thread GL calls via `-Dangelica.assertMainThread=true` + if(fontRenderer == null) { + try { + Field f = SplashProgress.class.getDeclaredField("fontRenderer"); + f.setAccessible(true); + fontRenderer = (FontRenderer)f.get(null); + } catch(ReflectiveOperationException e) { + AngelicaTweaker.LOGGER.error(e); + return; + } + } + glPushMatrix(); + glTranslatef(320 - Display.getWidth() / 2 + 4, 240 + Display.getHeight() / 2 - textHeight2, 0); + glScalef(2, 2, 1); + glEnable(GL_TEXTURE_2D); + fontRenderer.drawString("Angelica " + Tags.VERSION, 0, 0, 0x000000); + glDisable(GL_TEXTURE_2D); + glPopMatrix(); + if(AngelicaConfig.showSplashMemoryBar) { + glPushMatrix(); + glTranslatef(320 - (float) barWidth / 2, 20, 0); + drawMemoryBar(); + glPopMatrix(); + } + } + + @Shadow(remap = false) + private void setColor(int color) {} + + @Shadow(remap = false) + private void drawBox(int w, int h) {} + + @Final + @Shadow(remap = false) + private int barWidth, barHeight, textHeight2; + + private void drawMemoryBar() { + final int maxMemory = bytesToMb(Runtime.getRuntime().maxMemory()); + final int totalMemory = bytesToMb(Runtime.getRuntime().totalMemory()); + final int freeMemory = bytesToMb(Runtime.getRuntime().freeMemory()); + final int usedMemory = totalMemory - freeMemory; + final float usedMemoryPercent = usedMemory / (float) maxMemory; + + glPushMatrix(); + // title - message + setColor(AccessorSplashProgress.getFontColor()); + glScalef(2, 2, 1); + glEnable(GL_TEXTURE_2D); + fontRenderer.drawString("Memory Used / Total", 0, 0, 0x000000); + glDisable(GL_TEXTURE_2D); + glPopMatrix(); + // border + glPushMatrix(); + glTranslatef(0, textHeight2, 0); + setColor(AccessorSplashProgress.getBarBorderColor()); + drawBox(barWidth, barHeight); + // interior + setColor(AccessorSplashProgress.getBarBackgroundColor()); + glTranslatef(1, 1, 0); + drawBox(barWidth - 2, barHeight - 2); + // slidy part + + final long time = System.currentTimeMillis(); + if (usedMemoryPercent > memoryColorPercent || (time - memoryColorChangeTime > 1000)) { + memoryColorChangeTime = time; + memoryColorPercent = usedMemoryPercent; + } + + final int memoryBarColor; + if (memoryColorPercent < 0.75f) { + memoryBarColor = memoryGoodColor; + } else if (memoryColorPercent < 0.85f) { + memoryBarColor = memoryWarnColor; + } else { + memoryBarColor = memoryLowColor; + } + setColor(memoryLowColor); + glPushMatrix(); + glTranslatef((barWidth - 2) * (totalMemory) / (maxMemory) - 2, 0, 0); + drawBox(2, barHeight - 2); + glPopMatrix(); + setColor(memoryBarColor); + drawBox((barWidth - 2) * (usedMemory) / (maxMemory), barHeight - 2); + + // progress text + final String progress = getMemoryString(usedMemory) + " / " + getMemoryString(maxMemory); + glTranslatef(((float)barWidth - 2) / 2 - fontRenderer.getStringWidth(progress), 2, 0); + setColor(AccessorSplashProgress.getFontColor()); + glScalef(2, 2, 1); + glEnable(GL_TEXTURE_2D); + fontRenderer.drawString(progress, 0, 0, 0x000000); + glPopMatrix(); + } + + private String getMemoryString(int memory) + { + return StringUtils.leftPad(Integer.toString(memory), 4, ' ') + " MB"; + } + private static int bytesToMb(long bytes) + { + return (int) (bytes / 1024L / 1024L); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinThreadDownloadImageData.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinThreadDownloadImageData.java new file mode 100644 index 000000000..737052f62 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/archaic/MixinThreadDownloadImageData.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.archaic; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import net.minecraft.client.renderer.ThreadDownloadImageData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Removes a start of tons new threads + * for skins loading + */ +@Mixin(ThreadDownloadImageData.class) +public final class MixinThreadDownloadImageData { + private static final Executor EXECUTOR = Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() / 2), new ThreadFactoryBuilder().setNameFormat("Skin Downloader #%d").setDaemon(true).setPriority(Thread.MIN_PRIORITY).build()); + + @Redirect(method = "func_152433_a", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;start()V")) + private void onThreadStart(Thread thread) { + EXECUTOR.execute(thread); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java new file mode 100644 index 000000000..2634b1a95 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.debug; + +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import cpw.mods.fml.client.SplashProgress; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = SplashProgress.class, remap = false) +public class MixinSplashProgress { + @WrapOperation(method="Lcpw/mods/fml/client/SplashProgress;start()V", at=@At(value="INVOKE", target="Lcpw/mods/fml/client/SplashProgress;getBool(Ljava/lang/String;Z)Z")) + private static boolean angelica$disableSplashProgress(String name, boolean def, Operation original) { + AngelicaTweaker.LOGGER.info("Forcibly disabling splash progress because LWJGL debug has been set"); + // Forcibly disable splash progress until we can figure out why it's not working with our debug callback + if(name.equals("enabled")) + return false; + else + return original.call(name, def); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java new file mode 100644 index 000000000..df75a643e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java @@ -0,0 +1,178 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.fontrenderer; + +import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer; +import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Random; + +/** + * Fixes the horrible performance of FontRenderer + * @author eigenraven + */ +@Mixin(FontRenderer.class) +public abstract class MixinFontRenderer implements FontRendererAccessor { + + @Shadow + private boolean randomStyle; + + @Shadow + private boolean boldStyle; + + @Shadow + private boolean strikethroughStyle; + + @Shadow + private boolean underlineStyle; + + @Shadow + private boolean italicStyle; + + @Shadow + private int[] colorCode; + + @Shadow + private int textColor; + + @Shadow(remap = false) + protected abstract void setColor(float r, float g, float b, float a); + + @Shadow + private float alpha; + + @Shadow + private float red; + + /** Actually green */ + @Shadow + private float blue; + + /** Actually blue */ + @Shadow + private float green; + + @Shadow + public Random fontRandom; + + @Shadow + protected int[] charWidth; + + @Shadow + private boolean unicodeFlag; + + @Shadow + protected float posX; + + @Shadow + protected float posY; + + @Shadow + protected abstract float renderCharAtPos(int p_78278_1_, char p_78278_2_, boolean p_78278_3_); + + @Shadow(remap = false) + protected abstract void doDraw(float f); + + @Shadow + @Final + private static ResourceLocation[] unicodePageLocations; + @Shadow + protected byte[] glyphWidth; + @Shadow + @Final + protected ResourceLocation locationFontTexture; + @Shadow + @Final + private TextureManager renderEngine; + @Shadow + private boolean bidiFlag; + + @Shadow + protected abstract String bidiReorder(String p_147647_1_); + + @Shadow + protected abstract void bindTexture(ResourceLocation location); + + @Unique + public BatchingFontRenderer angelica$batcher; + + @Unique + private static final char angelica$FORMATTING_CHAR = 167; // § + + @Unique + private static final float angelica$1_over_255 = 1.0f/255.0f; // § + + @Inject(method = "", at = @At("TAIL")) + private void angelica$injectBatcher(GameSettings settings, ResourceLocation fontLocation, TextureManager texManager, + boolean unicodeMode, CallbackInfo ci) { + angelica$batcher = new BatchingFontRenderer((FontRenderer) (Object) this, unicodePageLocations, this.charWidth, this.glyphWidth, this.colorCode, this.locationFontTexture, this.renderEngine); + } + + @Unique + private static boolean angelica$charInRange(char what, char fromInclusive, char toInclusive) { + return (what >= fromInclusive) && (what <= toInclusive); + } + + /** + * @author eigenraven + * @reason Replace with more sensible batched rendering and optimize some operations + */ + @Overwrite + public int drawString(String text, int x, int y, int argb, boolean dropShadow) + { + if (text == null) + { + return 0; + } + else + { + if (this.bidiFlag) + { + text = this.bidiReorder(text); + } + + if ((argb & 0xfc000000) == 0) + { + argb |= 0xff000000; + } + + this.red = (float)(argb >> 16 & 255) / 255.0F; + this.blue = (float)(argb >> 8 & 255) / 255.0F; + this.green = (float)(argb & 255) / 255.0F; + this.alpha = (float)(argb >> 24 & 255) / 255.0F; + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + this.posX = (float)x; + this.posY = (float)y; + return (int) angelica$batcher.drawString(x, y, argb, dropShadow, unicodeFlag, text, 0, text.length()); + } + } + + /** + * @author eigenraven + * @reason Replace with more sensible batched rendering and optimize some operations + */ + @Overwrite + public int renderString(String text, int x, int y, int argb, boolean dropShadow) { + return drawString(text, x, y, argb, dropShadow); + } + + @Override + public BatchingFontRenderer angelica$getBatcher() { + return angelica$batcher; + } + + @Override + public void angelica$bindTexture(ResourceLocation location) { this.bindTexture(location); } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinGuiIngameForge.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinGuiIngameForge.java new file mode 100644 index 000000000..1754bd240 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinGuiIngameForge.java @@ -0,0 +1,36 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.fontrenderer; + +import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor; +import net.minecraft.client.gui.FontRenderer; +import net.minecraftforge.client.GuiIngameForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiIngameForge.class) +public class MixinGuiIngameForge { + + @Shadow(remap = false) + private FontRenderer fontrenderer; + + @Inject( + method = "renderHUDText", + at = @At( + value = "INVOKE", + target = "net/minecraftforge/client/event/RenderGameOverlayEvent$Text.(Lnet/minecraftforge/client/event/RenderGameOverlayEvent;Ljava/util/ArrayList;Ljava/util/ArrayList;)V"), + remap = false) + private void angelica$startF3TextBatching(int width, int height, CallbackInfo ci) { + FontRendererAccessor fra = (FontRendererAccessor) (Object) fontrenderer; + fra.angelica$getBatcher().beginBatch(); + } + + @Inject( + method = "renderHUDText", + at = @At(value = "INVOKE", target = "net/minecraft/profiler/Profiler.endSection()V")) + private void angelica$endF3TextBatching(int width, int height, CallbackInfo ci) { + FontRendererAccessor fra = (FontRendererAccessor) (Object) fontrenderer; + fra.angelica$getBatcher().endBatch(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/GuiIngameForgeAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/GuiIngameForgeAccessor.java new file mode 100644 index 000000000..8761bb0e9 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/GuiIngameForgeAccessor.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import net.minecraftforge.client.GuiIngameForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(GuiIngameForge.class) +public interface GuiIngameForgeAccessor { + + @Invoker(remap = false) + void callRenderCrosshairs(int width, int height); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinEntityRenderer_HUDCaching.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinEntityRenderer_HUDCaching.java new file mode 100644 index 000000000..4aa3c30a7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinEntityRenderer_HUDCaching.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import net.minecraft.client.gui.GuiIngame; +import net.minecraft.client.renderer.EntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = EntityRenderer.class) +public class MixinEntityRenderer_HUDCaching { + + @Redirect( + method = "updateCameraAndRender", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiIngame;renderGameOverlay(FZII)V")) + public void angelica$renderCachedHUD(GuiIngame guiIngame, float partialTicks, boolean b, int i, int j) { + HUDCaching.renderCachedHud((EntityRenderer) (Object) this, guiIngame, partialTicks, b, i, j); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinFramebuffer_HUDCaching.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinFramebuffer_HUDCaching.java new file mode 100644 index 000000000..68d4599c6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinFramebuffer_HUDCaching.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import net.minecraft.client.Minecraft; +import net.minecraft.client.shader.Framebuffer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Framebuffer.class) +public class MixinFramebuffer_HUDCaching { + + @Inject(method = "bindFramebuffer", at = @At("HEAD"), cancellable = true) + public void angelica$bindHUDCachingBuffer(boolean viewport, CallbackInfo ci) { + final Framebuffer framebuffer = (Framebuffer) (Object) this; + if (HUDCaching.renderingCacheOverride && framebuffer == Minecraft.getMinecraft().getFramebuffer()) { + HUDCaching.framebuffer.bindFramebuffer(viewport); + ci.cancel(); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngameForge_HUDCaching.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngameForge_HUDCaching.java new file mode 100644 index 000000000..337825adb --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngameForge_HUDCaching.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import net.minecraftforge.client.GuiIngameForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiIngameForge.class) +public class MixinGuiIngameForge_HUDCaching { + + @Inject(method = "renderCrosshairs", at = @At("HEAD"), cancellable = true, remap = false) + private void angelica$cancelCrosshair(CallbackInfo ci) { + if (HUDCaching.renderingCacheOverride) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngame_HUDCaching.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngame_HUDCaching.java new file mode 100644 index 000000000..f90120dea --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinGuiIngame_HUDCaching.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import net.minecraft.client.gui.GuiIngame; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiIngame.class) +public class MixinGuiIngame_HUDCaching { + + @Inject(method = "renderVignette", at = @At("HEAD"), cancellable = true) + private void angelica$cancelVignette(CallbackInfo ci) { + if (HUDCaching.renderingCacheOverride) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinRenderItem.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinRenderItem.java new file mode 100644 index 000000000..0418164cb --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/hudcaching/MixinRenderItem.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.hudcaching; + +import com.gtnewhorizons.angelica.mixins.dummy.DummyTessellator; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.RenderItem; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.Slice; + +import static org.lwjgl.opengl.GL11.GL_LIGHTING; + +@Mixin(RenderItem.class) +public class MixinRenderItem { + + @ModifyExpressionValue( + method = "renderItemIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IIZ)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/Tessellator;instance:Lnet/minecraft/client/renderer/Tessellator;") + ) + private Tessellator angelica$neuterTesselator(Tessellator useless) { + return DummyTessellator.instance; + } + + @Redirect( + method = "renderItemIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IIZ)V", + at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", remap = false), + slice = @Slice( + from = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;requiresMultipleRenderPasses()Z"), + to = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItem()Lnet/minecraft/item/Item;", ordinal = 4) + ) + ) + private void angelica$neuterGlDisable(int cap) { + if (cap == GL_LIGHTING) GL11.glDisable(cap); + } + + @Redirect( + method = "renderItemIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IIZ)V", + at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", remap = false), + slice = @Slice( + from = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", ordinal = 0), + to = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItem()Lnet/minecraft/item/Item;", ordinal = 4) + ) + ) + private void angelica$neuterGlEnable(int cap) {} + + @Redirect( + method = "renderItemIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IIZ)V", + at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glColorMask(ZZZZ)V", remap = false), + slice = @Slice( + from = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;requiresMultipleRenderPasses()Z"), + to = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItem()Lnet/minecraft/item/Item;", ordinal = 4) + ) + ) + private void angelica$neuterGlColorMask(boolean red, boolean green, boolean blue, boolean alpha) {} +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/lighting/MixinWorld_FixLightUpdateLag.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/lighting/MixinWorld_FixLightUpdateLag.java new file mode 100644 index 000000000..e01327618 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/lighting/MixinWorld_FixLightUpdateLag.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.lighting; + +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalIntRef; +import net.minecraft.world.EnumSkyBlock; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyConstant; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * This mixin is a backport of a Forge fix https://github.com/MinecraftForge/MinecraftForge/pull/4729 + */ +@Mixin(World.class) +public abstract class MixinWorld_FixLightUpdateLag { + + @Shadow + public abstract boolean doChunksNearChunkExist(int p_72873_1_, int p_72873_2_, int p_72873_3_, int p_72873_4_); + + @ModifyConstant(method = "updateLightByType", constant = @Constant(intValue = 17, ordinal = 0)) + public int hodgepodge$modifyRangeCheck1(int cst) { + return 16; + } + + @Inject(method = "updateLightByType", at = @At(value = "FIELD", target = "Lnet/minecraft/world/World;theProfiler:Lnet/minecraft/profiler/Profiler;", shift = At.Shift.BEFORE, ordinal = 0)) + public void hodgepodge$modifyUpdateRange(EnumSkyBlock p_147463_1_, int x, int y, int z, + CallbackInfoReturnable cir, @Share("updateRange") LocalIntRef updateRange) { + updateRange.set(this.doChunksNearChunkExist(x, y, z, 18) ? 17 : 15); + } + + @ModifyConstant(method = "updateLightByType", constant = { @Constant(intValue = 17, ordinal = 1), @Constant(intValue = 17, ordinal = 2) }) + public int hodgepodge$modifyRangeCheck2(int cst, @Share("updateRange") LocalIntRef updateRange) { + return updateRange.get(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinInitGLStateManager.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinInitGLStateManager.java new file mode 100644 index 000000000..f5129724c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinInitGLStateManager.java @@ -0,0 +1,16 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.startup; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.minecraft.client.renderer.OpenGlHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = OpenGlHelper.class, priority = 100) +public class MixinInitGLStateManager { + @Inject(method = "initializeTextures", at = @At("RETURN")) + private static void angelica$initializeGLStateManager(CallbackInfo ci) { + GLStateManager.init(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinSplashProgress.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinSplashProgress.java new file mode 100644 index 000000000..dd12d2316 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/startup/MixinSplashProgress.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.startup; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets="cpw/mods/fml/client/SplashProgress$3", remap = false) +public abstract class MixinSplashProgress { + @Inject(method = "setGL", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/locks/Lock;lock()V", shift = At.Shift.AFTER)) + private void angelica$startSplash(CallbackInfo ci) { + GLStateManager.setRunningSplash(true); + } + + @Inject(method = "clearGL", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/locks/Lock;unlock()V", shift = At.Shift.BEFORE)) + private void angelica$endSplash(CallbackInfo ci) { + GLStateManager.setRunningSplash(false); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureAtlasSprite.java new file mode 100644 index 000000000..d1d819200 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureAtlasSprite.java @@ -0,0 +1,34 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.textures; + +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(TextureAtlasSprite.class) +public abstract class MixinTextureAtlasSprite implements ISpriteExt { + @Shadow + private AnimationMetadataSection animationMetadata; + + @Shadow + protected int frameCounter; + + @Override + public boolean isAnimation() { + return animationMetadata != null && animationMetadata.getFrameCount() > 1; + } + + @Override + public int getFrame() { + return frameCounter; + } + + @Override + public void callUpload(int frameIndex) {} + + @Override + public AnimationMetadataSection getMetadata() { + return animationMetadata; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil.java new file mode 100644 index 000000000..dc030cb5f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.textures; + +import net.minecraft.client.renderer.texture.TextureUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(TextureUtil.class) +public class MixinTextureUtil { + + @Redirect(method = "allocateTextureImpl", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/TextureUtil;deleteTexture(I)V")) + private static void angelica$dontDeleteTexture(int textureId) { + // NO-OP - Not sure why it's deleting a texture that was just generated and subsequently being bound... + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil_OptimizeMipmap.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil_OptimizeMipmap.java new file mode 100644 index 000000000..d6d2a427f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/angelica/textures/MixinTextureUtil_OptimizeMipmap.java @@ -0,0 +1,71 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.textures; + +import com.gtnewhorizons.angelica.utils.Mipmaps; +import net.minecraft.client.renderer.texture.TextureUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(TextureUtil.class) +public class MixinTextureUtil_OptimizeMipmap { + + /** + * @author SuperCoder79 + * @reason Rewrite mipmap color math to use memoized value array instead of using Math.pow directly + */ + @Overwrite + private static int func_147943_a(int one, int two, int three, int four, boolean alpha) { + if (!alpha) { + final int a = Mipmaps.getColorComponent(one, two, three, four, 24); + final int r = Mipmaps.getColorComponent(one, two, three, four, 16); + final int g = Mipmaps.getColorComponent(one, two, three, four, 8); + final int b = Mipmaps.getColorComponent(one, two, three, four, 0); + return a << 24 | r << 16 | g << 8 | b; + } else { + float a = 0.0F; + float r = 0.0F; + float g = 0.0F; + float b = 0.0F; + if (one >> 24 != 0) { + a += Mipmaps.get(one >> 24); + r += Mipmaps.get(one >> 16); + g += Mipmaps.get(one >> 8); + b += Mipmaps.get(one >> 0); + } + + if (two >> 24 != 0) { + a += Mipmaps.get(two >> 24); + r += Mipmaps.get(two >> 16); + g += Mipmaps.get(two >> 8); + b += Mipmaps.get(two >> 0); + } + + if (three >> 24 != 0) { + a += Mipmaps.get(three >> 24); + r += Mipmaps.get(three >> 16); + g += Mipmaps.get(three >> 8); + b += Mipmaps.get(three >> 0); + } + + if (four >> 24 != 0) { + a += Mipmaps.get(four >> 24); + r += Mipmaps.get(four >> 16); + g += Mipmaps.get(four >> 8); + b += Mipmaps.get(four >> 0); + } + + a /= 4.0F; + r /= 4.0F; + g /= 4.0F; + b /= 4.0F; + int ia = (int) (Math.pow((double) a, 0.45454545454545453) * 255.0); + final int ir = (int) (Math.pow((double) r, 0.45454545454545453) * 255.0); + final int ig = (int) (Math.pow((double) g, 0.45454545454545453) * 255.0); + final int ib = (int) (Math.pow((double) b, 0.45454545454545453) * 255.0); + if (ia < 96) { + ia = 0; + } + + return ia << 24 | ir << 16 | ig << 8 | ib; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinBlock.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinBlock.java deleted file mode 100644 index 963b0e5a2..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinBlock.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.lighting; - -import net.minecraft.block.Block; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.Constant; -import org.spongepowered.asm.mixin.injection.ModifyConstant; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(Block.class) -public class MixinBlock { - - @ModifyConstant(method = "getAmbientOcclusionLightValue()F", constant = @Constant(floatValue = 0.2f)) - public float angelica$blockAoLight(float constant) { - return Shaders.blockAoLight; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinRenderBlocks.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinRenderBlocks.java deleted file mode 100644 index c2338547f..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/lighting/MixinRenderBlocks.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.lighting; - -import net.minecraft.client.renderer.RenderBlocks; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Constant; -import org.spongepowered.asm.mixin.injection.ModifyConstant; -import org.spongepowered.asm.mixin.injection.Slice; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderBlocks.class) -public class MixinRenderBlocks { - - // All of these expect 1 replacement only - @ModifyConstant( - method = { "renderBlockBed(Lnet/minecraft/block/Block;III)Z", - "renderBlockDoor(Lnet/minecraft/block/Block;III)Z", - "renderBlockLiquid(Lnet/minecraft/block/Block;III)Z", - "renderBlockCactusImpl(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithColorMultiplier(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderBlockSandFalling(Lnet/minecraft/block/Block;Lnet/minecraft/world/World;IIII)V" }, - constant = @Constant(floatValue = 0.5F), - expect = 1) - public float angelica$blockSingleLightLevel05(float constant) { - return Shaders.blockLightLevel05; - } - - @ModifyConstant( - method = { "renderBlockBed(Lnet/minecraft/block/Block;III)Z", - "renderBlockLiquid(Lnet/minecraft/block/Block;III)Z", - "renderBlockDoor(Lnet/minecraft/block/Block;III)Z", - "renderBlockCactusImpl(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithColorMultiplier(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderBlockSandFalling(Lnet/minecraft/block/Block;Lnet/minecraft/world/World;IIII)V" }, - constant = @Constant(floatValue = 0.6F), - expect = 1) - public float angelica$blockSingleLightLevel06(float constant) { - return Shaders.blockLightLevel06; - } - - @ModifyConstant( - method = { "renderBlockBed(Lnet/minecraft/block/Block;III)Z", - "renderBlockLiquid(Lnet/minecraft/block/Block;III)Z", - "renderBlockDoor(Lnet/minecraft/block/Block;III)Z", - "renderBlockCactusImpl(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithColorMultiplier(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderBlockSandFalling(Lnet/minecraft/block/Block;Lnet/minecraft/world/World;IIII)V" }, - constant = @Constant(floatValue = 0.8F), - expect = 1) - public float angelica$blockSingleLightLevel08(float constant) { - return Shaders.blockLightLevel08; - } - - // Piston Extension - Differing expectations, and slicing required - - @ModifyConstant( - method = "renderPistonExtension(Lnet/minecraft/block/Block;IIIZ)Z", - constant = @Constant(floatValue = 0.5F), - expect = 3, - slice = @Slice(from = @At(value = "CONSTANT", args = "doubleValue=8.0D", ordinal = 0))) - public float angelica$pistonBlockLightLevel05(float constant) { - return Shaders.blockLightLevel05; - } - - @ModifyConstant( - method = "renderPistonExtension(Lnet/minecraft/block/Block;IIIZ)Z", - constant = @Constant(floatValue = 0.6F), - expect = 12) - public float angelica$pistonBlockLightLevel06(float constant) { - return Shaders.blockLightLevel06; - } - - @ModifyConstant( - method = "renderPistonExtension(Lnet/minecraft/block/Block;IIIZ)Z", - constant = @Constant(floatValue = 0.8F), - expect = 4) - public float angelica$pistonBlockLightLevel08(float constant) { - return Shaders.blockLightLevel08; - } - - // BlockWithAmbientOcclusion - - @ModifyConstant( - method = { "renderStandardBlockWithAmbientOcclusionPartial(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithAmbientOcclusion(Lnet/minecraft/block/Block;IIIFFF)Z" }, - constant = @Constant(floatValue = 0.5F)) - public float angelica$multipleBlockLightLevel05(float constant) { - return Shaders.blockLightLevel05; - } - - @ModifyConstant( - method = { "renderStandardBlockWithAmbientOcclusionPartial(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithAmbientOcclusion(Lnet/minecraft/block/Block;IIIFFF)Z" }, - constant = @Constant(floatValue = 0.6F)) - public float angelica$multipleBlockLightLevel06(float constant) { - return Shaders.blockLightLevel06; - } - - @ModifyConstant( - method = { "renderStandardBlockWithAmbientOcclusionPartial(Lnet/minecraft/block/Block;IIIFFF)Z", - "renderStandardBlockWithAmbientOcclusion(Lnet/minecraft/block/Block;IIIFFF)Z" }, - constant = @Constant(floatValue = 0.8F)) - public float angelica$multipleBlockLightLevel08(float constant) { - return Shaders.blockLightLevel08; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinEntityRenderer.java new file mode 100644 index 000000000..c659a1578 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinEntityRenderer.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.clouds; + +import jss.notfine.core.SettingsManager; +import net.minecraft.client.renderer.EntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(value = EntityRenderer.class, priority = 990) +public abstract class MixinEntityRenderer { + + @ModifyConstant(method = "renderWorld", constant = @Constant(doubleValue = 128.0D), expect = 2) + double notFine$modifyCloudHeightCheck(double original) { + return SettingsManager.cloudTranslucencyCheck; + } + + @ModifyArg(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false), index = 3) + private float notFine$modifyFarPlane(float original) { + return Math.max(original, SettingsManager.minimumFarPlaneDistance); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinGameSettings.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinGameSettings.java new file mode 100644 index 000000000..ad1a7a76f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinGameSettings.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.clouds; + +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = GameSettings.class) +public abstract class MixinGameSettings { + + /** + * @author jss2a98aj + * @reason Make clouds drawScreen at any drawScreen distance. + */ + @Overwrite + public boolean shouldRenderClouds() { + return clouds; + } + + @Shadow public boolean clouds; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinRenderGlobal.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinRenderGlobal.java new file mode 100644 index 000000000..1c97ba3e3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinRenderGlobal.java @@ -0,0 +1,255 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.clouds; + +import jss.notfine.core.Settings; +import jss.notfine.gui.options.named.GraphicsQualityOff; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.Vec3; +import net.minecraftforge.client.IRenderHandler; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = RenderGlobal.class, priority = 990) +public abstract class MixinRenderGlobal { + + /** + * @author jss2a98aj + * @reason Adjust how cloud drawScreen mode is selected. + */ + @Overwrite + public void renderClouds(float partialTicks) { + IRenderHandler renderer; + if((renderer = theWorld.provider.getCloudRenderer()) != null) { + renderer.render(partialTicks, theWorld, mc); + return; + } + if(mc.theWorld.provider.isSurfaceWorld()) { + GraphicsQualityOff cloudMode = (GraphicsQualityOff)Settings.MODE_CLOUDS.option.getStore(); + if(cloudMode == GraphicsQualityOff.FANCY || cloudMode == GraphicsQualityOff.DEFAULT && mc.gameSettings.fancyGraphics) { + renderCloudsFancy(partialTicks); + } else { + renderCloudsFast(partialTicks); + } + } + } + + /** + * @author jss2a98aj + * @reason Adjust fancy cloud drawScreen. + */ + @Overwrite + public void renderCloudsFancy(float partialTicks) { + Tessellator tessellator = Tessellator.instance; + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + renderEngine.bindTexture(locationCloudsPng); + + Vec3 color = theWorld.getCloudColour(partialTicks); + float red = (float)color.xCoord; + float green = (float)color.yCoord; + float blue = (float)color.zCoord; + if(mc.gameSettings.anaglyph) { + float altRed = (red * 30.0F + green * 59.0F + blue * 11.0F) / 100.0F; + float altGreen = (red * 30.0F + green * 70.0F) / 100.0F; + float altBlue = (red * 30.0F + blue * 70.0F) / 100.0F; + red = altRed; + green = altGreen; + blue = altBlue; + } + double cloudTick = ((float)cloudTickCounter + partialTicks); + + float cloudScale = (int)Settings.CLOUD_SCALE.option.getStore() * 0.25f; + float cloudInteriorWidth = 12.0F * cloudScale; + float cloudInteriorHeight = 4.0F * cloudScale; + float cameraOffsetY = (float)(mc.renderViewEntity.lastTickPosY + (mc.renderViewEntity.posY - mc.renderViewEntity.lastTickPosY) * (double)partialTicks); + double cameraOffsetX = (mc.renderViewEntity.prevPosX + (mc.renderViewEntity.posX - mc.renderViewEntity.prevPosX) * (double)partialTicks + cloudTick * 0.03D) / (double)cloudInteriorWidth; + double cameraOffsetZ = (mc.renderViewEntity.prevPosZ + (mc.renderViewEntity.posZ - mc.renderViewEntity.prevPosZ) * (double)partialTicks) / (double)cloudInteriorWidth + 0.33D; + cameraOffsetX -= MathHelper.floor_double(cameraOffsetX / 2048.0D) * 2048; + cameraOffsetZ -= MathHelper.floor_double(cameraOffsetZ / 2048.0D) * 2048; + + float cameraRelativeY = theWorld.provider.getCloudHeight() - cameraOffsetY + 0.33F; + float cameraRelativeX = (float)(cameraOffsetX - (double)MathHelper.floor_double(cameraOffsetX)); + float cameraRelativeZ = (float)(cameraOffsetZ - (double)MathHelper.floor_double(cameraOffsetZ)); + + float scrollSpeed = 0.00390625F; + float cloudScrollingX = (float)MathHelper.floor_double(cameraOffsetX) * scrollSpeed; + float cloudScrollingZ = (float)MathHelper.floor_double(cameraOffsetZ) * scrollSpeed; + + float cloudWidth = 8f; + int renderRadius = (int)((int)Settings.RENDER_DISTANCE_CLOUDS.option.getStore() / (cloudScale * 2f)); + float edgeOverlap = 0.0001f;//0.001F; + GL11.glScalef(cloudInteriorWidth, 1.0F, cloudInteriorWidth); + + for (int loop = 0; loop < 2; ++loop) { + if (loop == 0) { + GL11.glColorMask(false, false, false, false); + } else if (mc.gameSettings.anaglyph) { + if (EntityRenderer.anaglyphField == 0) { + GL11.glColorMask(false, true, true, true); + } else { + GL11.glColorMask(true, false, false, true); + } + } else { + GL11.glColorMask(true, true, true, true); + } + + for(int chunkX = -renderRadius + 1; chunkX <= renderRadius; ++chunkX) { + for(int chunkZ = -renderRadius + 1; chunkZ <= renderRadius; ++chunkZ) { + tessellator.startDrawingQuads(); + float chunkOffsetX = (chunkX * cloudWidth); + float chunkOffsetZ = (chunkZ * cloudWidth); + float startX = chunkOffsetX - cameraRelativeX; + float startZ = chunkOffsetZ - cameraRelativeZ; + + //Cloud top + if(cameraRelativeY > -cloudInteriorHeight - 1.0F) { + tessellator.setColorRGBA_F(red * 0.7F, green * 0.7F, blue * 0.7F, 0.8F); + tessellator.setNormal(0.0F, -1.0F, 0.0F); + tessellator.addVertexWithUV(startX, cameraRelativeY, (startZ + cloudWidth), (chunkOffsetX * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), cameraRelativeY, (startZ + cloudWidth), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), cameraRelativeY, startZ, ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), (chunkOffsetZ * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(startX, cameraRelativeY, startZ, (chunkOffsetX * scrollSpeed + cloudScrollingX), (chunkOffsetZ * scrollSpeed + cloudScrollingZ)); + } + //Cloud bottom + if(cameraRelativeY <= cloudInteriorHeight + 1.0F) { + tessellator.setColorRGBA_F(red, green, blue, 0.8F); + tessellator.setNormal(0.0F, 1.0F, 0.0F); + tessellator.addVertexWithUV(startX, (cameraRelativeY + cloudInteriorHeight - edgeOverlap), (startZ + cloudWidth), ((chunkOffsetX) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), (cameraRelativeY + cloudInteriorHeight - edgeOverlap), (startZ + cloudWidth), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), (cameraRelativeY + cloudInteriorHeight - edgeOverlap), startZ, ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), (chunkOffsetZ * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(startX, (cameraRelativeY + cloudInteriorHeight - edgeOverlap), startZ, (chunkOffsetX * scrollSpeed + cloudScrollingX), (chunkOffsetZ * scrollSpeed + cloudScrollingZ)); + } + + tessellator.setColorRGBA_F(red * 0.9F, green * 0.9F, blue * 0.9F, 0.8F); + if(Math.abs(chunkX) < 6 && Math.abs(chunkZ) < 6) { + float chunk; + + if (chunkX > -1) { + tessellator.setNormal(-1.0F, 0.0F, 0.0F); + for (chunk = 0f; chunk < cloudWidth; ++chunk) { + double x = startX + chunk; + tessellator.addVertexWithUV(x, cameraRelativeY, (startZ + cloudWidth), ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, (cameraRelativeY + cloudInteriorHeight), (startZ + cloudWidth), ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, (cameraRelativeY + cloudInteriorHeight), startZ, ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, cameraRelativeY, startZ, ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ) * scrollSpeed + cloudScrollingZ)); + } + } + + if (chunkX <= 1) { + tessellator.setNormal(1.0F, 0.0F, 0.0F); + for (chunk = 0f; chunk < cloudWidth; ++chunk) { + double x = startX + chunk + 1.0F - edgeOverlap; + tessellator.addVertexWithUV(x, cameraRelativeY, (startZ + cloudWidth), ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, (cameraRelativeY + cloudInteriorHeight), (startZ + cloudWidth), ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + cloudWidth) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, (cameraRelativeY + cloudInteriorHeight), startZ, ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(x, cameraRelativeY, startZ, ((chunkOffsetX + chunk + 0.5F) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ) * scrollSpeed + cloudScrollingZ)); + } + } + + tessellator.setColorRGBA_F(red * 0.8F, green * 0.8F, blue * 0.8F, 0.8F); + + if (chunkZ > -1) { + tessellator.setNormal(0.0F, 0.0F, -1.0F); + for (chunk = 0f; chunk < cloudWidth; ++chunk) { + tessellator.addVertexWithUV(startX, (cameraRelativeY + cloudInteriorHeight), (startZ + chunk), ((chunkOffsetX) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), (cameraRelativeY + cloudInteriorHeight), (startZ + chunk), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), (cameraRelativeY), (startZ + chunk), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(startX, (cameraRelativeY), (startZ + chunk), ((chunkOffsetX) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + } + } + + if (chunkZ <= 1) { + tessellator.setNormal(0.0F, 0.0F, 1.0F); + for (chunk = 0f; chunk < cloudWidth; ++chunk) { + tessellator.addVertexWithUV(startX, (cameraRelativeY + cloudInteriorHeight), (startZ + chunk + 1.0F - edgeOverlap), (chunkOffsetX * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), (cameraRelativeY + cloudInteriorHeight), (startZ + chunk + 1.0F - edgeOverlap), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV((startX + cloudWidth), cameraRelativeY, (startZ + chunk + 1.0F - edgeOverlap), ((chunkOffsetX + cloudWidth) * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + tessellator.addVertexWithUV(startX, (cameraRelativeY), (startZ + chunk + 1.0F - edgeOverlap), (chunkOffsetX * scrollSpeed + cloudScrollingX), ((chunkOffsetZ + chunk + 0.5F) * scrollSpeed + cloudScrollingZ)); + } + } + } + tessellator.draw(); + } + } + } + + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + } + + public void renderCloudsFast(float partialTicks) { + Tessellator tessellator = Tessellator.instance; + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + renderEngine.bindTexture(locationCloudsPng); + + Vec3 color = theWorld.getCloudColour(partialTicks); + float red = (float)color.xCoord; + float green = (float)color.yCoord; + float blue = (float)color.zCoord; + if(mc.gameSettings.anaglyph) { + float altRed = (red * 30.0F + green * 59.0F + blue * 11.0F) / 100.0F; + float altGreen = (red * 30.0F + green * 70.0F) / 100.0F; + float altBlue = (red * 30.0F + blue * 70.0F) / 100.0F; + red = altRed; + green = altGreen; + blue = altBlue; + } + double cloudTick = ((float)cloudTickCounter + partialTicks); + float cameraOffsetY = (float)(mc.renderViewEntity.lastTickPosY + (mc.renderViewEntity.posY - mc.renderViewEntity.lastTickPosY) * (double)partialTicks); + double cameraOffsetX = mc.renderViewEntity.prevPosX + (mc.renderViewEntity.posX - mc.renderViewEntity.prevPosX) * (double)partialTicks + cloudTick * 0.03D; + double cameraOffsetZ = mc.renderViewEntity.prevPosZ + (mc.renderViewEntity.posZ - mc.renderViewEntity.prevPosZ) * (double)partialTicks; + cameraOffsetX -= MathHelper.floor_double(cameraOffsetX / 2048.0D) * 2048; + cameraOffsetZ -= MathHelper.floor_double(cameraOffsetZ / 2048.0D) * 2048; + + float renderRadius = 32 * (int)Settings.RENDER_DISTANCE_CLOUDS.option.getStore(); + double uvScale = 0.0005D / (int)Settings.CLOUD_SCALE.option.getStore() * 0.25f; + + float uvShiftX = (float)(cameraOffsetX * uvScale); + float uvShiftZ = (float)(cameraOffsetZ * uvScale); + + double cameraRelativeY = theWorld.provider.getCloudHeight() - cameraOffsetY + 0.33F; + double neg = -renderRadius; + double pos = renderRadius; + + double startXUv = neg * uvScale + uvShiftX; + double startZUv = neg * uvScale + uvShiftZ; + double movedXUv = pos * uvScale + uvShiftX; + double movedZUv = pos * uvScale + uvShiftZ; + + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(red, green, blue, 0.8F); + tessellator.addVertexWithUV(neg, cameraRelativeY, pos, startXUv, movedZUv); + tessellator.addVertexWithUV(pos, cameraRelativeY, pos, movedXUv, movedZUv); + tessellator.addVertexWithUV(pos, cameraRelativeY, neg, movedXUv, startZUv); + tessellator.addVertexWithUV(neg, cameraRelativeY, neg, startXUv, startZUv); + tessellator.draw(); + + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + } + + @Shadow @Final + private static ResourceLocation locationCloudsPng; + @Shadow @Final + private TextureManager renderEngine; + + @Shadow private WorldClient theWorld; + @Shadow private Minecraft mc; + @Shadow private int cloudTickCounter; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinWorldType.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinWorldType.java new file mode 100644 index 000000000..8adce4cfe --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/clouds/MixinWorldType.java @@ -0,0 +1,20 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.clouds; + +import jss.notfine.core.Settings; +import net.minecraft.world.WorldType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(value = WorldType.class) +public abstract class MixinWorldType { + + /** + * @author jss2a98aj + * @reason Control cloud height. + */ + @Overwrite(remap = false) + public float getCloudHeight() { + return (int)Settings.CLOUD_HEIGHT.option.getStore(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlock.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlock.java new file mode 100644 index 000000000..4402922e3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlock.java @@ -0,0 +1,75 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.faceculling; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import jss.notfine.util.IFaceObstructionCheckHelper; +import net.minecraft.block.Block; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = Block.class) +public abstract class MixinBlock { + + /** + * @author jss2a98aj + * @reason More accurate face culling. + */ + @Overwrite() + @SideOnly(Side.CLIENT) + public boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) { + //Check if side is touching another block + switch(side) { + case 0 -> { + if (minY > 0.0D) { + return true; + } + } + case 1 -> { + if (maxY < 1.0D) { + return true; + } + } + case 2 -> { + if (minZ > 0.0D) { + return true; + } + } + case 3 -> { + if (maxZ < 1.0D) { + return true; + } + } + case 4 -> { + if (minX > 0.0D) { + return true; + } + } + case 5 -> { + if (maxX < 1.0D) { + return true; + } + } + } + //Check if other block is solid + Block otherBlock = worldIn.getBlock(x, y, z); + if(otherBlock.isOpaqueCube()) { + return false; + } + //Check for IFaceObstructionCheckHelper + if(otherBlock instanceof IFaceObstructionCheckHelper target) { + return target.isFaceNonObstructing(worldIn, x, y, z, side, minX, minY, minZ, maxX, maxY, maxZ); + } + //Default + return true; + } + + @Shadow protected double minX; + @Shadow protected double minY; + @Shadow protected double minZ; + @Shadow protected double maxX; + @Shadow protected double maxY; + @Shadow protected double maxZ; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSlab.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSlab.java new file mode 100644 index 000000000..42e43e9e0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSlab.java @@ -0,0 +1,67 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.faceculling; + +import jss.notfine.util.IFaceObstructionCheckHelper; +import net.minecraft.block.Block; +import net.minecraft.block.BlockSlab; +import net.minecraft.block.material.Material; +import net.minecraft.util.Facing; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = BlockSlab.class) +public abstract class MixinBlockSlab extends Block implements IFaceObstructionCheckHelper { + + public boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) { + //If the slab is not a full cube and this is up or down + if(!field_150004_a && side <= 1) { + int thisX = x + Facing.offsetsXForSide[Facing.oppositeSide[side]]; + int thisY = y + Facing.offsetsYForSide[Facing.oppositeSide[side]]; + int thisZ = z + Facing.offsetsZForSide[Facing.oppositeSide[side]]; + if((worldIn.getBlockMetadata(thisX, thisY, thisZ) & 8) != side) { + return true; + } + } + //Check if other block is solid + Block otherBlock = worldIn.getBlock(x, y, z); + if(otherBlock.isOpaqueCube()) { + return false; + } + //Check for IFaceObstructionCheckHelper + if(otherBlock instanceof IFaceObstructionCheckHelper target) { + return target.isFaceNonObstructing(worldIn, x, y, z, side, minX, minY, minZ, maxX, maxY, maxZ); + } + //Default + return true; + } + + /** + * @author jss2a98aj + * @reason Improved accuracy in the case a mod ever uses this. + */ + @Overwrite() + private static boolean func_150003_a(Block block) { + return block.getRenderBlockPass() == 0 && block instanceof BlockSlab; + } + + @Override() + public boolean isFaceNonObstructing(IBlockAccess worldIn, int x, int y, int z, int side, double otherMinX, double otherMinY, double otherMinZ, double otherMaxX, double otherMaxY, double otherMaxZ) { + if(getRenderBlockPass() == 1) { + return true; + } + boolean isTop = (worldIn.getBlockMetadata(x, y, z) & 8) != 0; + return switch (side) { + // -y bottom + case 0 -> !isTop; + // +y top + case 1 -> isTop; + default -> isTop ? otherMinY < 0.5D : otherMaxY > 0.5D; + }; + } + + MixinBlockSlab(Material material) { super(material); } + + @Shadow @Final protected boolean field_150004_a; +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSnow.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSnow.java new file mode 100644 index 000000000..97c7b0d24 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockSnow.java @@ -0,0 +1,60 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.faceculling; + +import jss.notfine.util.IFaceObstructionCheckHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.block.Block; +import net.minecraft.block.BlockSnow; +import net.minecraft.block.material.Material; +import net.minecraft.world.IBlockAccess; + +@Mixin(value = BlockSnow.class) +public abstract class MixinBlockSnow extends Block implements IFaceObstructionCheckHelper { + + MixinBlockSnow(Material material) { + super(material); + } + + /** + * @author jss2a98aj + * @reason More accurate face culling. + */ + @Overwrite() + @SideOnly(Side.CLIENT) + public boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) { + //If this is the top and this is not full height + if(side == 1 && maxY < 1.0D) { + return true; + } + //Check if other block is solid + Block otherBlock = worldIn.getBlock(x, y, z); + if(otherBlock.isOpaqueCube()) { + return false; + } + //Check for IFaceObstructionCheckHelper + if(otherBlock instanceof IFaceObstructionCheckHelper target) { + return target.isFaceNonObstructing(worldIn, x, y, z, side, minX, minY, minZ, maxX, maxY, maxZ); + } + //Default + return true; + } + + @Override() + public boolean isFaceNonObstructing(IBlockAccess worldIn, int x, int y, int z, int side, double otherMinX, double otherMinY, double otherMinZ, double otherMaxX, double otherMaxY, double otherMaxZ) { + if(getRenderBlockPass() == 1) { + return true; + } + int meta = worldIn.getBlockMetadata(x, y, z) & 7; + return switch (side) { + // +y top + case 0 -> meta != 7; + // -y bottom + case 1 -> false; + default -> otherMaxY > (float) (2 * (1 + meta)) * 0.0625f; + }; + } +} + diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockStairs.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockStairs.java new file mode 100644 index 000000000..526d2ae62 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/faceculling/MixinBlockStairs.java @@ -0,0 +1,32 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.faceculling; + +import jss.notfine.util.IFaceObstructionCheckHelper; +import net.minecraft.block.Block; +import net.minecraft.block.BlockStairs; +import net.minecraft.block.material.Material; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(value = BlockStairs.class) +public abstract class MixinBlockStairs extends Block implements IFaceObstructionCheckHelper { + + @Override() + public boolean isFaceNonObstructing(IBlockAccess worldIn, int x, int y, int z, int side, double otherMinX, double otherMinY, double otherMinZ, double otherMaxX, double otherMaxY, double otherMaxZ) { + if(getRenderBlockPass() == 1) { + return true; + } + boolean isTop = (worldIn.getBlockMetadata(x, y, z) & 7) > 3; + return switch (side) { + // -y bottom + case 0 -> !isTop; + // +y top + case 1 -> isTop; + default -> isTop ? otherMinY < 0.5D : otherMaxY > 0.5D; + }; + } + + private MixinBlockStairs(Material materialIn) { + super(materialIn); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinItemRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinItemRenderer.java new file mode 100644 index 000000000..c5ee0efbf --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinItemRenderer.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.glint; + +import jss.notfine.core.Settings; +import net.minecraft.client.renderer.ItemRenderer; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = ItemRenderer.class) +public abstract class MixinItemRenderer { + + @Redirect( + method = "renderItem(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/item/ItemStack;ILnet/minecraftforge/client/IItemRenderer$ItemRenderType;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;hasEffect(I)Z" + ), + remap = false + ) + private boolean notFine$toggleGlint(ItemStack stack, int pass) { + return (boolean)Settings.MODE_GLINT_WORLD.option.getStore() && stack.hasEffect(pass); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderBiped.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderBiped.java new file mode 100644 index 000000000..62c5d73ed --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderBiped.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.glint; + +import jss.notfine.core.Settings; +import net.minecraft.client.renderer.entity.RenderBiped; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = RenderBiped.class) +public abstract class MixinRenderBiped { + + @Redirect( + method = "shouldRenderPass(Lnet/minecraft/entity/EntityLiving;IF)I", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;isItemEnchanted()Z" + ) + ) + private boolean notFine$toggleGlint(ItemStack stack) { + return (boolean)Settings.MODE_GLINT_WORLD.option.getStore() && stack.isItemEnchanted(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderItem.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderItem.java new file mode 100644 index 000000000..a9cc227a7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderItem.java @@ -0,0 +1,78 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.glint; + +import jss.notfine.core.Settings; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = RenderItem.class) +public abstract class MixinRenderItem { + + /** + * @author jss2a98aj + * @reason Makes renderGlint faster and fixes glBlendFunc being left with the wrong values. + */ + @Overwrite + private void renderGlint(int unused, int posX, int posY, int width, int height) { + if(!(boolean)Settings.MODE_GLINT_INV.option.getStore()) { + return; + } + final float timeUVSpeed = 0.00390625F; + final Tessellator tessellator = Tessellator.instance; + final long time = Minecraft.getSystemTime(); + + float layerUVNoise = 4.0F; + + OpenGlHelper.glBlendFunc(772, 1, 0, 1); + + //for(int layer = 0; layer < 2; ++layer) { + final int timeUVDenominator = 3000 /*+ layer * 1873*/; + final float timeUVNoise = (float)(time % (long)timeUVDenominator) / (float)timeUVDenominator * 256F; + + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV( + posX, (posY + height), zLevel, + ((timeUVNoise + (float)height * layerUVNoise) * timeUVSpeed), ((float)height * timeUVSpeed) + ); + tessellator.addVertexWithUV( + (posX + width), (posY + height), zLevel, + ((timeUVNoise + (float)width + (float)height * layerUVNoise) * timeUVSpeed), ((float)height * timeUVSpeed) + ); + tessellator.addVertexWithUV( + (posX + width), posY, zLevel, + ((timeUVNoise + (float)width) * timeUVSpeed), 0D + ); + tessellator.addVertexWithUV( + posX, posY, zLevel, + (timeUVNoise * timeUVSpeed), 0D + ); + tessellator.draw(); + + //layerUVNoise = -1.0F; + //} + + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + } + + @Redirect( + method = "renderDroppedItem(Lnet/minecraft/entity/item/EntityItem;Lnet/minecraft/util/IIcon;IFFFFI)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;hasEffect(I)Z" + ), + remap = false + ) + private boolean notFine$toggleGlint(ItemStack stack, int pass) { + return stack.hasEffect(pass) && (boolean)Settings.MODE_GLINT_WORLD.option.getStore(); + } + + @Shadow public float zLevel; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderPlayer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderPlayer.java new file mode 100644 index 000000000..c73b84c08 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/glint/MixinRenderPlayer.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.glint; + +import jss.notfine.core.Settings; +import net.minecraft.client.renderer.entity.RenderPlayer; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = RenderPlayer.class) +public abstract class MixinRenderPlayer { + + @Redirect( + method = "shouldRenderPass(Lnet/minecraft/client/entity/AbstractClientPlayer;IF)I", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;isItemEnchanted()Z" + ) + ) + private boolean notFine$toggleGlint(ItemStack stack) { + return stack.isItemEnchanted() && (boolean)Settings.MODE_GLINT_WORLD.option.getStore(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/gui/MixinGuiSlot.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/gui/MixinGuiSlot.java new file mode 100644 index 000000000..e97cbd1bc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/gui/MixinGuiSlot.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.gui; + +import jss.notfine.core.Settings; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiSlot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiSlot.class) +public abstract class MixinGuiSlot { + + @Inject(method = "drawContainerBackground", at = @At("HEAD"), cancellable = true, remap = false) + private void notFine$toggleContainerBackground(CallbackInfo ci) { + if(!(boolean)Settings.MODE_GUI_BACKGROUND.option.getStore() && Minecraft.getMinecraft().theWorld != null) { + ci.cancel(); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeaves.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeaves.java new file mode 100644 index 000000000..ba1194887 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeaves.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.leaves; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import net.minecraft.block.BlockLeaves; +import net.minecraft.block.BlockLeavesBase; +import net.minecraft.block.material.Material; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = BlockLeaves.class) +public abstract class MixinBlockLeaves extends BlockLeavesBase { + + /** + * @author jss2a98aj + * @reason Control leaf opacity. + */ + @Overwrite + public boolean isOpaqueCube() { + return SettingsManager.leavesOpaque; + } + + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + if(field_150129_M[0] == null) { + //A mod dev had no idea what they were doing. + return getIcon(side, world.getBlockMetadata(x, y, z)); + } + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + int maskedMeta = world.getBlockMetadata(x, y, z) & 3; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + maskedMeta = maskedMeta >= field_150129_M[renderMode].length ? 0 : maskedMeta; + return field_150129_M[renderMode][maskedMeta]; + } + + @Shadow protected IIcon[][] field_150129_M; + + private MixinBlockLeaves(Material material, boolean overridden) { + super(material, overridden); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeavesBase.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeavesBase.java new file mode 100644 index 000000000..8583e3c4f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/leaves/MixinBlockLeavesBase.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.leaves; + +import jss.notfine.util.ILeafBlock; +import jss.notfine.util.LeafRenderUtil; +import net.minecraft.block.Block; +import net.minecraft.block.BlockLeavesBase; +import net.minecraft.block.material.Material; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(value = BlockLeavesBase.class) +public abstract class MixinBlockLeavesBase extends Block implements ILeafBlock { + + @Override + public boolean shouldSideBeRendered(IBlockAccess world, int x, int y, int z, int side) { + return LeafRenderUtil.shouldSideBeRendered(world, x, y, z, side); + } + + private MixinBlockLeavesBase(Material material) { + super(material); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinBlockEnchantmentTable.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinBlockEnchantmentTable.java new file mode 100644 index 000000000..2e165daac --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinBlockEnchantmentTable.java @@ -0,0 +1,61 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.particles; + +import java.util.Random; + +import jss.notfine.core.Settings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import net.minecraft.block.BlockContainer; +import net.minecraft.block.BlockEnchantmentTable; +import net.minecraft.block.material.Material; +import net.minecraft.world.World; + + +@Mixin(value = BlockEnchantmentTable.class) +public abstract class MixinBlockEnchantmentTable extends BlockContainer { + + /** + * @author jss2a98aj + * @reason Makes anything with enchantment power cause enchantment particles. + */ + @Overwrite() + public void randomDisplayTick(World world, int x, int y, int z, Random rand) { + //Unneeded in vanilla as the super call is empty. + //super.randomDisplayTick(world, x, y, z, rand); + float particleChance = (int)Settings.PARTICLES_ENC_TABLE.option.getStore(); + if(particleChance <= 0f) { + return; + } + for (int xPos = x - 2; xPos <= x + 2; ++xPos) { + for (int zPos = z - 2; zPos <= z + 2; ++zPos) { + if (xPos > x - 2 && xPos < x + 2 && zPos == z - 1) { + zPos = z + 2; + } + if (rand.nextInt(16) <= particleChance) { + for (int yPos = y; yPos <= y + 1; ++yPos) { + if (world.getBlock(xPos, yPos, zPos).getEnchantPowerBonus(world, xPos, yPos, zPos) > 0f) { + if (!world.isAirBlock((xPos - x) / 2 + x, yPos, (zPos - z) / 2 + z)) { + break; + } + world.spawnParticle( + "enchantmenttable", + (double)x + 0.5D, + (double)y + 2.0D, + (double)z + 0.5D, + (double)((float)(xPos - x) + rand.nextFloat()) - 0.5D, + (float)(yPos - y) - rand.nextFloat() - 1.0F, + (double)((float)(zPos - z) + rand.nextFloat()) - 0.5D + ); + } + } + } + } + } + } + + private MixinBlockEnchantmentTable(Material material) { + super(material); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinEffectRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinEffectRenderer.java new file mode 100644 index 000000000..148659868 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinEffectRenderer.java @@ -0,0 +1,27 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.particles; + +import net.minecraft.client.particle.EffectRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = EffectRenderer.class) +public abstract class MixinEffectRenderer { + + /** + * @author jss2a98aj + * @reason Makes most particles drawScreen with the expected depth. + */ + @Redirect( + method = "renderParticles", + at = @At( + value = "INVOKE", + target = "Lorg/lwjgl/opengl/GL11;glDepthMask(Z)V", + ordinal = 0, + remap = false + ) + ) + private void skipGlDepthMask(boolean flag) { + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldClient.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldClient.java new file mode 100644 index 000000000..b0bf70020 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldClient.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.particles; + +import java.util.Random; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import jss.util.RandomXoshiro256StarStar; +import net.minecraft.client.multiplayer.WorldClient; + +@Mixin(value = WorldClient.class) +public abstract class MixinWorldClient { + + /** + * @author jss2a98aj + * @reason Xoshiro256** is faster than Random. + */ + @Redirect(method = "doVoidFogParticles(III)V", at = @At(value = "NEW", target = "java/util/Random", ordinal = 0)) + private Random notFine$redirectDoVoidFogParticlesRandom() { + return new RandomXoshiro256StarStar(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldProvider.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldProvider.java new file mode 100644 index 000000000..c5043a06b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/particles/MixinWorldProvider.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.particles; + +import jss.notfine.core.Settings; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = WorldProvider.class) +public class MixinWorldProvider { + + /** + * @author jss2a98aj + * @reason Toggle void particles. + */ + @Overwrite + public boolean getWorldHasVoidParticles() { + return (boolean)Settings.PARTICLES_VOID.option.getStore() && terrainType.hasVoidParticles(hasNoSky); + } + + @Shadow public WorldType terrainType; + @Shadow public boolean hasNoSky; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/renderer/MixinRenderGlobal.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/renderer/MixinRenderGlobal.java new file mode 100644 index 000000000..b060d308a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/renderer/MixinRenderGlobal.java @@ -0,0 +1,29 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.renderer; + +import jss.notfine.core.Settings; +import jss.notfine.render.RenderStars; +import net.minecraft.client.renderer.RenderGlobal; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = RenderGlobal.class) +public abstract class MixinRenderGlobal { + + @Inject(method = "renderSky", at = @At("HEAD"), cancellable = true) + void notFine$toggleSky(CallbackInfo ci) { + if(!(boolean)Settings.MODE_SKY.option.getStore()) ci.cancel(); + } + + /** + * @author jss2a98aj + * @reason Control star generation. + */ + @Overwrite + private void renderStars() { + RenderStars.renderStars(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/settings/MixinGameSettings.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/settings/MixinGameSettings.java new file mode 100644 index 000000000..f1e31ff71 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/settings/MixinGameSettings.java @@ -0,0 +1,235 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.settings; + +import jss.notfine.core.SettingsManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.EnumDifficulty; +import org.lwjgl.opengl.Display; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = GameSettings.class, priority = 990) +public abstract class MixinGameSettings { + + @Final + private static String[] GUISCALES = new String[] { + "options.guiScale.auto", + "options.guiScale.small", + "options.guiScale.normal", + "options.guiScale.large", + "options.guiScale.massive" + }; + + /** + * @author jss2a98aj + * @reason Makes this function not unreasonably slow. + */ + @Overwrite + public void setOptionFloatValue(GameSettings.Options option, float value) { + switch (option) { + case SENSITIVITY -> mouseSensitivity = value; + case FOV -> fovSetting = value; + case GAMMA -> gammaSetting = value; + case FRAMERATE_LIMIT -> limitFramerate = (int) value; + case CHAT_OPACITY -> { + chatOpacity = value; + mc.ingameGUI.getChatGUI().refreshChat(); + } + case CHAT_HEIGHT_FOCUSED -> { + chatHeightFocused = value; + mc.ingameGUI.getChatGUI().refreshChat(); + } + case CHAT_HEIGHT_UNFOCUSED -> { + chatHeightUnfocused = value; + mc.ingameGUI.getChatGUI().refreshChat(); + } + case CHAT_WIDTH -> { + chatWidth = value; + mc.ingameGUI.getChatGUI().refreshChat(); + } + case CHAT_SCALE -> { + chatScale = value; + mc.ingameGUI.getChatGUI().refreshChat(); + } + case ANISOTROPIC_FILTERING -> { + if (anisotropicFiltering != (int) value) { + anisotropicFiltering = (int) value; + mc.getTextureMapBlocks().setAnisotropicFiltering(this.anisotropicFiltering); + mc.scheduleResourcesRefresh(); + } + } + case MIPMAP_LEVELS -> { + if (mipmapLevels != (int) value) { + mipmapLevels = (int) value; + mc.getTextureMapBlocks().setMipmapLevels(this.mipmapLevels); + mc.scheduleResourcesRefresh(); + } + } + case RENDER_DISTANCE -> renderDistanceChunks = (int) value; + case STREAM_BYTES_PER_PIXEL -> field_152400_J = value; + case STREAM_VOLUME_MIC -> { + field_152401_K = value; + mc.func_152346_Z().func_152915_s(); + } + case STREAM_VOLUME_SYSTEM -> { + field_152402_L = value; + mc.func_152346_Z().func_152915_s(); + } + case STREAM_KBPS -> field_152403_M = value; + case STREAM_FPS -> field_152404_N = value; + } + } + + /** + * @author jss2a98aj + * @reason Makes this function not unreasonably slow. + */ + @Overwrite + public void setOptionValue(GameSettings.Options option, int value) { + switch(option) { + case INVERT_MOUSE: + invertMouse = !invertMouse; + break; + case GUI_SCALE: + guiScale = (guiScale + value) % 5; + break; + case PARTICLES: + particleSetting = (particleSetting + value) % 3; + break; + case VIEW_BOBBING: + viewBobbing = !viewBobbing; + break; + case RENDER_CLOUDS: + clouds = !clouds; + break; + case FORCE_UNICODE_FONT: + forceUnicodeFont = !forceUnicodeFont; + mc.fontRenderer.setUnicodeFlag(mc.getLanguageManager().isCurrentLocaleUnicode() || forceUnicodeFont); + break; + case ADVANCED_OPENGL: + advancedOpengl = !advancedOpengl; + mc.renderGlobal.loadRenderers(); + case FBO_ENABLE: + fboEnable = !fboEnable; + break; + case ANAGLYPH: + anaglyph = !anaglyph; + //refreshResources was overkill. + mc.renderGlobal.loadRenderers(); + break; + case DIFFICULTY: + difficulty = EnumDifficulty.getDifficultyEnum(difficulty.getDifficultyId() + value & 3); + break; + case GRAPHICS: + fancyGraphics = !fancyGraphics; + SettingsManager.graphicsUpdated(); + mc.renderGlobal.loadRenderers(); + break; + case AMBIENT_OCCLUSION: + ambientOcclusion = (ambientOcclusion + value) % 3; + mc.renderGlobal.loadRenderers(); + break; + case CHAT_VISIBILITY: + chatVisibility = EntityPlayer.EnumChatVisibility.getEnumChatVisibility((chatVisibility.getChatVisibility() + value) % 3); + break; + case STREAM_COMPRESSION: + field_152405_O = (field_152405_O + value) % 3; + break; + case STREAM_SEND_METADATA: + field_152406_P = !field_152406_P; + break; + case STREAM_CHAT_ENABLED: + field_152408_R = (field_152408_R + value) % 3; + break; + case STREAM_CHAT_USER_FILTER: + field_152409_S = (field_152409_S + value) % 3; + break; + case STREAM_MIC_TOGGLE_BEHAVIOR: + field_152410_T = (field_152410_T + value) % 2; + break; + case CHAT_COLOR: + chatColours = !chatColours; + break; + case CHAT_LINKS: + chatLinks = !chatLinks; + break; + case CHAT_LINKS_PROMPT: + chatLinksPrompt = !chatLinksPrompt; + break; + case SNOOPER_ENABLED: + snooperEnabled = !snooperEnabled; + break; + case SHOW_CAPE: + showCape = !showCape; + break; + case TOUCHSCREEN: + touchscreen = !touchscreen; + break; + case USE_FULLSCREEN: + fullScreen = !fullScreen; + if (mc.isFullScreen() != fullScreen) { + mc.toggleFullscreen(); + } + break; + case ENABLE_VSYNC: + enableVsync = !enableVsync; + Display.setVSyncEnabled(enableVsync); + break; + } + saveOptions(); + } + + @Shadow + public void saveOptions() { + } + + @Shadow protected Minecraft mc; + + @Shadow public float mouseSensitivity; + @Shadow public boolean invertMouse; + @Shadow public float fovSetting; + @Shadow public float gammaSetting; + @Shadow public int renderDistanceChunks; + @Shadow public int guiScale; + @Shadow public int particleSetting; + @Shadow public boolean viewBobbing; + @Shadow public boolean anaglyph; + @Shadow public boolean advancedOpengl; + @Shadow public int limitFramerate; + @Shadow public boolean fboEnable; + @Shadow public EnumDifficulty difficulty; + @Shadow public boolean fancyGraphics; + @Shadow public int ambientOcclusion; + @Shadow public boolean clouds; + @Shadow public EntityPlayer.EnumChatVisibility chatVisibility; + @Shadow public boolean chatColours; + @Shadow public boolean chatLinks; + @Shadow public boolean chatLinksPrompt; + @Shadow public float chatOpacity; + @Shadow public boolean snooperEnabled; + @Shadow public boolean fullScreen; + @Shadow public boolean enableVsync; + @Shadow public boolean showCape; + @Shadow public boolean touchscreen; + @Shadow public float chatHeightFocused; + @Shadow public float chatHeightUnfocused; + @Shadow public float chatScale; + @Shadow public float chatWidth; + @Shadow public int mipmapLevels; + @Shadow public int anisotropicFiltering; + @Shadow public float field_152400_J; + @Shadow public float field_152401_K; + @Shadow public float field_152402_L; + @Shadow public float field_152403_M; + @Shadow public float field_152404_N; + @Shadow public int field_152405_O; + @Shadow public boolean field_152406_P; + @Shadow public int field_152408_R; + @Shadow public int field_152409_S; + @Shadow public int field_152410_T; + @Shadow public boolean forceUnicodeFont; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinEntityRenderer.java new file mode 100644 index 000000000..6ec1b85ba --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinEntityRenderer.java @@ -0,0 +1,50 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.toggle; + +import jss.notfine.core.SettingsManager; + +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(EntityRenderer.class) +abstract public class MixinEntityRenderer { + + @Redirect( + method = "renderWorld(FJ)V", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", + opcode = Opcodes.GETFIELD, + ordinal = 0 + ) + ) + private boolean notFine$toggleWaterDetail(GameSettings settings) { + return SettingsManager.waterDetail; + } + + @Redirect( + method = "renderRainSnow(F)V", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", + opcode = Opcodes.GETFIELD + ) + ) + private boolean notFine$bypassWeatherMode(GameSettings settings) { + return false; + } + + @ModifyVariable( + method = "renderRainSnow(F)V", + at = @At("STORE"), + ordinal = 0 + ) + private byte notFine$weatherDistance(byte distance) { + return SettingsManager.downfallDistance; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinGuiIngame.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinGuiIngame.java new file mode 100644 index 000000000..04be41fc7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinGuiIngame.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.toggle; + +import jss.notfine.core.SettingsManager; +import net.minecraft.client.gui.GuiIngame; +import net.minecraftforge.client.GuiIngameForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = {GuiIngame.class, GuiIngameForge.class}) +public abstract class MixinGuiIngame { + + @Redirect( + method = "renderGameOverlay(FZII)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/Minecraft;isFancyGraphicsEnabled()Z" + ) + ) + private boolean notFine$toggleVignette(float whyAndHowIsThisAFloat) { + return SettingsManager.vignette; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRender.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRender.java new file mode 100644 index 000000000..de551a3a1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRender.java @@ -0,0 +1,27 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.toggle; + +import jss.notfine.core.SettingsManager; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = Render.class) +public abstract class MixinRender { + + @Redirect( + method = "doRenderShadowAndFire", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", + opcode = Opcodes.GETFIELD, + ordinal = 0 + ) + ) + private boolean notFine$toggleEntityShadows(GameSettings settings) { + return SettingsManager.shadows; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRenderItem.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRenderItem.java new file mode 100644 index 000000000..7927253e0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/notfine/toggle/MixinRenderItem.java @@ -0,0 +1,27 @@ +package com.gtnewhorizons.angelica.mixins.early.notfine.toggle; + +import jss.notfine.core.SettingsManager; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(RenderItem.class) +abstract public class MixinRenderItem { + + @Redirect( + method = "renderDroppedItem(Lnet/minecraft/entity/item/EntityItem;Lnet/minecraft/util/IIcon;IFFFFI)V", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", + opcode = Opcodes.GETFIELD + ), + allow = 1 + ) + private boolean notFine$toggleDroppedItemDetail(GameSettings settings) { + return SettingsManager.droppedItemDetail; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinAbstractTexture.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinAbstractTexture.java deleted file mode 100644 index eb0abca22..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinAbstractTexture.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.AbstractTexture; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.SoftOverride; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.MultiTexID; -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(AbstractTexture.class) -public class MixinAbstractTexture { - - public MultiTexID angelica$multiTex; - - @Inject(at = @At("HEAD"), method = "deleteGlTexture()V") - private void angelica$deleteTextures(CallbackInfo ci) { - ShadersTex.deleteTextures((AbstractTexture) (Object) this); - } - - @SoftOverride - public MultiTexID angelica$getMultiTexID() { - return ShadersTex.getMultiTexID((AbstractTexture) (Object) this); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinDynamicTexture.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinDynamicTexture.java deleted file mode 100644 index 38fae2bf5..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinDynamicTexture.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.DynamicTexture; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyVariable; -import org.spongepowered.asm.mixin.injection.Redirect; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(DynamicTexture.class) -public class MixinDynamicTexture { - - @ModifyVariable(argsOnly = true, at = @At(ordinal = 1, value = "LOAD"), index = 1, method = "(II)V") - private int angelica$resizeDynamicTextureData(int p_i1271_2_) { - return p_i1271_2_ * 3; - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;allocateTexture(III)V", - value = "INVOKE"), - method = "(II)V") - private void angelica$initDynamicTexture(int p_110991_0_, int p_110991_1_, int p_110991_2_) { - // p_110991_1_ needs to be divided by 3 to revert angelica$resizeDynamicTextureData - ShadersTex.initDynamicTexture(p_110991_0_, p_110991_1_ / 3, p_110991_2_, (DynamicTexture) (Object) this); - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTexture(I[III)V", - value = "INVOKE"), - method = "updateDynamicTexture()V") - private void angelica$updateDynamicTexture(int p_110988_0_, int[] p_110988_1_, int p_110988_2_, int p_110988_3_) { - ShadersTex.updateDynamicTexture( - p_110988_0_, - p_110988_1_, - p_110988_2_, - p_110988_3_, - (DynamicTexture) (Object) this); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinEntityRenderer.java deleted file mode 100644 index ccf6f30cc..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinEntityRenderer.java +++ /dev/null @@ -1,408 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import java.nio.FloatBuffer; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.particle.EffectRenderer; -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.ItemRenderer; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.culling.Frustrum; -import net.minecraft.client.renderer.culling.ICamera; -import net.minecraft.client.settings.GameSettings; -import net.minecraft.entity.EntityLivingBase; - -import org.lwjgl.opengl.GL11; -import org.spongepowered.asm.lib.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.Slice; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import com.gtnewhorizon.mixinextras.injector.ModifyExpressionValue; -import com.gtnewhorizons.angelica.client.Shaders; -import com.gtnewhorizons.angelica.client.ShadersRender; - -@Mixin(EntityRenderer.class) -public abstract class MixinEntityRenderer { - - @Shadow - private Minecraft mc; - - @Shadow - public abstract void disableLightmap(double p_78483_1_); - - // renderHand - - @Inject( - method = "renderHand(FI)V", - at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) - private void angelica$applyHandDepth(CallbackInfo ci) { - Shaders.applyHandDepth(); - } - - @Inject( - method = "renderHand(FI)V", - at = @At( - value = "FIELD", - target = "Lnet/minecraft/client/settings/GameSettings;thirdPersonView:I", - ordinal = 1), - cancellable = true) - private void angelica$checkCompositeRendered(float p_78476_1_, int p_78476_2_, CallbackInfo ci) { - if (!Shaders.isCompositeRendered) { - ci.cancel(); - } - this.disableLightmap(p_78476_1_); - } - - @Redirect( - method = "renderHand(FI)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/ItemRenderer;renderItemInFirstPerson(F)V")) - private void angelica$renderItemInFirstPerson(ItemRenderer itemRenderer, float partialTicks) { - ShadersRender.renderItemFP(itemRenderer, partialTicks); - } - - // disableLightmap - - @Inject(at = @At("RETURN"), method = "disableLightmap(D)V") - private void angelica$disableLightmap(CallbackInfo ci) { - Shaders.disableLightmap(); - } - - // enableLightmap - - @Inject(at = @At("RETURN"), method = "enableLightmap(D)V") - private void angelica$enableLightmap(CallbackInfo ci) { - Shaders.enableLightmap(); - } - - // renderWorld - - @Inject(at = @At("HEAD"), method = "renderWorld(FJ)V") - private void angelica$beginRender(float p_78471_1_, long p_78471_2_, CallbackInfo ci) { - Shaders.beginRender(this.mc, p_78471_1_, p_78471_2_); - } - - @Redirect( - at = @At(remap = false, target = "Lorg/lwjgl/opengl/GL11;glViewport(IIII)V", value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$setViewport(int x, int y, int width, int height) { - Shaders.setViewport(x, y, width, height); - } - - @Inject( - at = @At( - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glClear(I)V", - value = "INVOKE"), - method = "renderWorld(FJ)V", - slice = @Slice( - from = @At( - args = "intValue=" + (GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT), - value = "CONSTANT"), - to = @At(args = "intValue=" + GL11.GL_CULL_FACE, ordinal = 1, value = "CONSTANT"))) - private void angelica$clearRenderBuffer(CallbackInfo ci) { - Shaders.clearRenderBuffer(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/EntityRenderer;setupCameraTransform(FI)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$setCamera(float p_78471_1_, long p_78471_2_, CallbackInfo ci) { - Shaders.setCamera(p_78471_1_); - } - - @Redirect( - at = @At( - opcode = Opcodes.GETFIELD, - target = "Lnet/minecraft/client/settings/GameSettings;renderDistanceChunks:I", - value = "FIELD"), - method = "renderWorld(FJ)V") - private int angelica$isShadowPass(GameSettings gameSettings) { - // A better way would be replacing the if-expression completely but I don't know how. If you figure it how, - // please let me (glowredman) know. Unless that happens, we just redirect the field access and return either 3 - // or 4 to achieve the same. - return Shaders.isShadowPass ? 3 : 4; - } - - @Inject( - at = @At(target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(F)V", value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginSky(CallbackInfo ci) { - Shaders.beginSky(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$endSky(CallbackInfo ci) { - Shaders.endSky(); - } - - @Redirect( - at = @At(target = "Lnet/minecraft/client/renderer/culling/Frustrum;setPosition(DDD)V", value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$setFrustrumPosition(Frustrum frustrum, double x, double y, double z) { - ShadersRender.setFrustrumPosition(frustrum, x, y, z); - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/RenderGlobal;clipRenderersByFrustum(Lnet/minecraft/client/renderer/culling/ICamera;F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$clipRenderersByFrustrum(RenderGlobal renderGlobal, ICamera p_72729_1_, float p_72729_2_) { - ShadersRender.clipRenderersByFrustrum(renderGlobal, (Frustrum) p_72729_1_, p_72729_2_); - } - - @Inject( - at = @At( - ordinal = 7, - target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginUpdateChunks(CallbackInfo ci) { - Shaders.beginUpdateChunks(); - } - - @Inject( - at = @At( - opcode = Opcodes.GETFIELD, - ordinal = 1, - target = "Lnet/minecraft/entity/EntityLivingBase;posY:D", - value = "FIELD"), - locals = LocalCapture.CAPTURE_FAILEXCEPTION, - method = "renderWorld(FJ)V") - private void angelica$endUpdateChunks(float p_78471_1_, long p_78471_2_, CallbackInfo ci, - EntityLivingBase entitylivingbase, RenderGlobal renderglobal, EffectRenderer effectrenderer, double d0, - double d1, double d2, int j) { - // A better solution would be to inject this directly at the end of the "if (j == 0)" block so checking j == 0 - // again here isn't necessary. If you figure out how to do that, please let me (glowredman) know. - if (j == 0) { - Shaders.endUpdateChunks(); - } - } - - @Inject( - at = @At( - ordinal = 0, - target = "Lnet/minecraft/client/renderer/RenderGlobal;sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginTerrain(CallbackInfo ci) { - Shaders.beginTerrain(); - } - - @Inject( - at = @At( - ordinal = 0, - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/RenderGlobal;sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$endTerrain(CallbackInfo ci) { - Shaders.endTerrain(); - } - - @Inject( - at = @At( - target = "Lnet/minecraft/client/particle/EffectRenderer;renderLitParticles(Lnet/minecraft/entity/Entity;F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginLitParticles(CallbackInfo ci) { - Shaders.beginLitParticles(); - } - - @Inject( - at = @At( - target = "Lnet/minecraft/client/particle/EffectRenderer;renderParticles(Lnet/minecraft/entity/Entity;F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginParticles(CallbackInfo ci) { - Shaders.beginParticles(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/particle/EffectRenderer;renderParticles(Lnet/minecraft/entity/Entity;F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$endParticles(CallbackInfo ci) { - Shaders.endParticles(); - } - - @Inject( - at = @At(target = "Lnet/minecraft/client/renderer/EntityRenderer;renderRainSnow(F)V", value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$beginWeather(CallbackInfo ci) { - Shaders.beginWeather(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/EntityRenderer;renderRainSnow(F)V", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$endWeather(CallbackInfo ci) { - Shaders.endWeather(); - } - - @Inject( - at = @At( - ordinal = 1, - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glDepthMask(Z)V", - value = "INVOKE"), - locals = LocalCapture.CAPTURE_FAILEXCEPTION, - method = "renderWorld(FJ)V") - private void angelica$renderHand0AndPreWater(float p_78471_1_, long p_78471_2_, CallbackInfo ci, - EntityLivingBase entitylivingbase, RenderGlobal renderglobal, EffectRenderer effectrenderer, double d0, - double d1, double d2, int j) { - ShadersRender.renderHand0((EntityRenderer) (Object) this, p_78471_1_, j); - Shaders.preWater(); - } - - @Inject( - at = @At( - target = "Lnet/minecraft/client/renderer/RenderGlobal;sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - value = "INVOKE"), - method = "renderWorld(FJ)V", - slice = @Slice(from = @At(args = "stringValue=water", ordinal = 0, value = "CONSTANT"))) - private void angelica$beginWater(CallbackInfo ci) { - Shaders.beginWater(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/RenderGlobal;sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - value = "INVOKE"), - method = "renderWorld(FJ)V", - slice = @Slice(from = @At(args = "stringValue=water", ordinal = 0, value = "CONSTANT"))) - private void angelica$endWater(CallbackInfo ci) { - Shaders.endWater(); - } - - @Inject( - at = @At( - opcode = Opcodes.GETFIELD, - target = "Lnet/minecraft/entity/EntityLivingBase;posY:D", - value = "FIELD"), - method = "renderWorld(FJ)V", - slice = @Slice( - from = @At(args = "stringValue=entities", value = "CONSTANT"), - to = @At(args = "stringValue=aboveClouds", value = "CONSTANT"))) - private void angelica$disableFog(CallbackInfo ci) { - Shaders.disableFog(); - } - - @ModifyExpressionValue( - at = @At( - remap = false, - target = "Lnet/minecraftforge/client/ForgeHooksClient;renderFirstPersonHand(Lnet/minecraft/client/renderer/RenderGlobal;FI)Z", - value = "INVOKE"), - method = "renderWorld(FJ)V") - private boolean angelica$isShadowPass(boolean renderFirstPersonHand) { - return renderFirstPersonHand || Shaders.isShadowPass; - } - - @Inject( - at = @At(remap = false, target = "Lorg/lwjgl/opengl/GL11;glClear(I)V", value = "INVOKE"), - locals = LocalCapture.CAPTURE_FAILEXCEPTION, - method = "renderWorld(FJ)V", - slice = @Slice(from = @At(args = "stringValue=hand", value = "CONSTANT"))) - private void angelica$renderHand1AndRenderCompositeFinal(float p_78471_1_, long p_78471_2_, CallbackInfo ci, - EntityLivingBase entitylivingbase, RenderGlobal renderglobal, EffectRenderer effectrenderer, double d0, - double d1, double d2, int j) { - ShadersRender.renderHand1((EntityRenderer) (Object) this, p_78471_1_, j); - Shaders.renderCompositeFinal(); - } - - @Redirect( - at = @At(target = "Lnet/minecraft/client/renderer/EntityRenderer;renderHand(FI)V", value = "INVOKE"), - method = "renderWorld(FJ)V") - private void angelica$renderFPOverlay(EntityRenderer thizz, float p_78476_1_, int p_78476_2_) { - ShadersRender.renderFPOverlay(thizz, p_78476_1_, p_78476_2_); - } - - @Inject( - at = @At( - opcode = Opcodes.GETFIELD, - ordinal = 0, - target = "Lnet/minecraft/client/renderer/EntityRenderer;mc:Lnet/minecraft/client/Minecraft;", - value = "FIELD"), - method = "renderWorld(FJ)V", - slice = @Slice(from = @At(args = "stringValue=hand", value = "CONSTANT"))) - private void angelica$endRender(CallbackInfo ci) { - Shaders.endRender(); - } - - // renderCloudsCheck - - @Redirect( - at = @At(target = "Lnet/minecraft/client/settings/GameSettings;shouldRenderClouds()Z", value = "INVOKE"), - method = "renderCloudsCheck(Lnet/minecraft/client/renderer/RenderGlobal;F)V") - private boolean angelica$shouldRenderClouds(GameSettings gameSettings) { - return Shaders.shouldRenderClouds(gameSettings); - } - - @Inject( - at = @At(target = "Lnet/minecraft/client/renderer/RenderGlobal;renderClouds(F)V", value = "INVOKE"), - method = "renderCloudsCheck(Lnet/minecraft/client/renderer/RenderGlobal;F)V") - private void angelica$beginClouds(CallbackInfo ci) { - Shaders.beginClouds(); - } - - @Inject( - at = @At( - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/RenderGlobal;renderClouds(F)V", - value = "INVOKE"), - method = "renderCloudsCheck(Lnet/minecraft/client/renderer/RenderGlobal;F)V") - private void angelica$endClouds(CallbackInfo ci) { - Shaders.endClouds(); - } - - // updateFogColor - - @Redirect( - at = @At(remap = false, target = "Lorg/lwjgl/opengl/GL11;glClearColor(FFFF)V", value = "INVOKE"), - method = "updateFogColor(F)V") - private void angelica$setClearColor(float red, float green, float blue, float alpha) { - Shaders.setClearColor(red, green, blue, alpha); - } - - // setupFog - - @Redirect( - at = @At(remap = false, target = "Lorg/lwjgl/opengl/GL11;glFogi(II)V", value = "INVOKE"), - method = "setupFog(IF)V") - private void angelica$sglFogi(int pname, int param) { - Shaders.sglFogi(pname, param); - } - - // setFogColorBuffer - - @Inject(at = @At("HEAD"), method = "setFogColorBuffer(FFFF)Ljava/nio/FloatBuffer;") - private void angelica$setFogColor(float red, float green, float blue, float alpha, - CallbackInfoReturnable cir) { - Shaders.setFogColor(red, green, blue); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinITextureObject.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinITextureObject.java deleted file mode 100644 index 5e54da1b2..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinITextureObject.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.ITextureObject; - -import org.spongepowered.asm.mixin.Mixin; - -import com.gtnewhorizons.angelica.client.MultiTexID; - -@Mixin(ITextureObject.class) -public interface MixinITextureObject { - - MultiTexID angelica$getMultiTexID(); - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinItemRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinItemRenderer.java deleted file mode 100644 index 11aa5830a..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinItemRenderer.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.entity.EntityClientPlayerMP; -import net.minecraft.client.renderer.ItemRenderer; -import net.minecraft.item.ItemStack; - -import org.spongepowered.asm.lib.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(ItemRenderer.class) -public class MixinItemRenderer { - - @Inject( - at = @At( - opcode = Opcodes.PUTFIELD, - target = "Lnet/minecraft/client/renderer/ItemRenderer;itemToRender:Lnet/minecraft/item/ItemStack;", - value = "FIELD"), - locals = LocalCapture.CAPTURE_FAILEXCEPTION, - method = "updateEquippedItem()V") - private void angelica$setItemToRender(CallbackInfo ci, EntityClientPlayerMP entityclientplayermp, - ItemStack itemstack) { - Shaders.itemToRender = itemstack; - } - - @ModifyArg( - at = @At( - ordinal = 0, - remap = true, - target = "Lnet/minecraft/client/renderer/OpenGlHelper;glBlendFunc(IIII)V", - value = "INVOKE"), - index = 3, - method = "renderItem(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/item/ItemStack;ILnet/minecraftforge/client/IItemRenderer$ItemRenderType;)V", - remap = false) - private int angelica$adjust_dfactorAlpha(int original) { - return 1; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinLayeredTexture.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinLayeredTexture.java deleted file mode 100644 index 51759a3ee..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinLayeredTexture.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import java.io.IOException; -import java.util.List; - -import net.minecraft.client.renderer.texture.LayeredTexture; -import net.minecraft.client.resources.IResourceManager; - -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; -import org.spongepowered.asm.mixin.Shadow; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(LayeredTexture.class) -public class MixinLayeredTexture { - - @Final - @Shadow - public List layeredTextureNames; - - /** - * @author glowredman - * @reason must take normal and specular maps into account - */ - @Overwrite - public void loadTexture(IResourceManager p_110551_1_) throws IOException { - ShadersTex.loadLayeredTexture((LayeredTexture) (Object) this, p_110551_1_, this.layeredTextureNames); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinModelRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinModelRenderer.java deleted file mode 100644 index a71e868e9..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinModelRenderer.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.model.ModelRenderer; -import net.minecraft.client.renderer.GLAllocation; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -import com.gtnewhorizons.angelica.mixins.interfaces.IModelRenderer; - -@Mixin(ModelRenderer.class) -public class MixinModelRenderer implements IModelRenderer { - - @Shadow - private boolean compiled; - - @Shadow - private int displayList; - - public void angelica$resetDisplayList() { - if (!compiled && displayList != 0) { - GLAllocation.deleteDisplayLists(displayList); - displayList = 0; - compiled = false; - } - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinOpenGlHelper.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinOpenGlHelper.java deleted file mode 100644 index 023c2cd4d..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinOpenGlHelper.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.OpenGlHelper; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(OpenGlHelper.class) -public class MixinOpenGlHelper { - - @Inject(method = "setActiveTexture(I)V", at = @At("HEAD")) - private static void angelica$setActiveTexUnit(int activeTexture, CallbackInfo ci) { - Shaders.activeTexUnit = activeTexture; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderBlocks.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderBlocks.java deleted file mode 100644 index 23ac47815..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderBlocks.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.block.Block; -import net.minecraft.block.BlockFlowerPot; -import net.minecraft.client.renderer.RenderBlocks; -import net.minecraft.item.Item; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import com.gtnewhorizon.mixinextras.injector.wrapoperation.Operation; -import com.gtnewhorizon.mixinextras.injector.wrapoperation.WrapOperation; -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderBlocks.class) -public class MixinRenderBlocks { - - // Block - @Inject(method = "renderBlockByRenderType(Lnet/minecraft/block/Block;III)Z", at = @At("HEAD")) - private void angelica$pushEntityBlock(Block block, int x, int y, int z, CallbackInfoReturnable cir) { - Shaders.pushEntity((RenderBlocks) ((Object) this), block, x, y, z); - } - - @Inject(method = "renderBlockByRenderType(Lnet/minecraft/block/Block;III)Z", at = @At("RETURN")) - private void angelica$popEntityBlock(Block block, int x, int y, int z, CallbackInfoReturnable cir) { - Shaders.popEntity(); - } - - // BlockFlowerPot - @Inject(method = "renderBlockFlowerpot(Lnet/minecraft/block/BlockFlowerPot;III)Z", at = @At("HEAD")) - private void angelica$pushEntityBlockFlowerPot(BlockFlowerPot blockFlowerPot, int x, int y, int z, - CallbackInfoReturnable cir) { - Shaders.pushEntity((RenderBlocks) ((Object) this), blockFlowerPot, x, y, z); - } - - @Inject(method = "renderBlockFlowerpot(Lnet/minecraft/block/BlockFlowerPot;III)Z", at = @At("RETURN")) - private void angelica$popEntityBlockFlowerPot(BlockFlowerPot blockFlowerPot, int x, int y, int z, - CallbackInfoReturnable cir) { - Shaders.popEntity(); - } - - @WrapOperation( - method = "renderBlockFlowerpot(Lnet/minecraft/block/BlockFlowerPot;III)Z", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/block/Block;getBlockFromItem(Lnet/minecraft/item/Item;)Lnet/minecraft/block/Block;")) - private Block angelica$pushEntityFlowerInFlowerPot(Item itemIn, Operation original) { - final Block block = original.call(itemIn); - Shaders.pushEntity(block); - return block; - } - - @Inject( - method = "renderBlockFlowerpot(Lnet/minecraft/block/BlockFlowerPot;III)Z", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/Tessellator;addTranslation(FFF)V", - shift = At.Shift.AFTER, - ordinal = 1)) - private void angelica$popEntityFlowerInFlowerPot(BlockFlowerPot blockFlowerPot, int x, int y, int z, - CallbackInfoReturnable cir) { - Shaders.popEntity(); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderDragon.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderDragon.java deleted file mode 100644 index eb795ccee..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderDragon.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.entity.RenderDragon; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderDragon.class) -public class MixinRenderDragon { - - @Inject( - at = @At( - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glColor4f(FFFF)V", - value = "INVOKE"), - method = "shouldRenderPass(Lnet/minecraft/entity/boss/EntityDragon;IF)I") - private void angelica$beginSpiderEyes(CallbackInfoReturnable cir) { - Shaders.beginSpiderEyes(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderEnderman.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderEnderman.java deleted file mode 100644 index 903e87d0e..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderEnderman.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.entity.RenderEnderman; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderEnderman.class) -public class MixinRenderEnderman { - - @Inject( - at = @At( - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glColor4f(FFFF)V", - value = "INVOKE"), - method = "shouldRenderPass(Lnet/minecraft/entity/monster/EntityEnderman;IF)I") - private void angelica$beginSpiderEyes(CallbackInfoReturnable cir) { - Shaders.beginSpiderEyes(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderGlobal.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderGlobal.java deleted file mode 100644 index a634152da..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderGlobal.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.renderer.culling.ICamera; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.util.MovingObjectPosition; - -import org.lwjgl.opengl.GL11; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import com.gtnewhorizon.mixinextras.injector.wrapoperation.Operation; -import com.gtnewhorizon.mixinextras.injector.wrapoperation.WrapOperation; -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderGlobal.class) -public class MixinRenderGlobal { - - @Inject( - method = "renderEntities(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/client/renderer/culling/ICamera;F)V", - at = @At( - value = "INVOKE_STRING", - target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", - args = "ldc=entities")) - private void angelica$beginEntities(EntityLivingBase entity, ICamera camera, float p_147589_3_, CallbackInfo ci) { - Shaders.beginEntities(); - } - - @Inject( - method = "renderEntities(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/client/renderer/culling/ICamera;F)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntitySimple(Lnet/minecraft/entity/Entity;F)Z", - ordinal = 1, - shift = At.Shift.AFTER)) - private void angelica$nextEntity(EntityLivingBase entity, ICamera camera, float p_147589_3_, CallbackInfo ci) { - Shaders.nextEntity(); - } - - @Inject( - method = "renderEntities(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/client/renderer/culling/ICamera;F)V", - at = @At( - value = "INVOKE_STRING", - target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", - args = "ldc=blockentities", - shift = At.Shift.AFTER)) - private void angelica$endEntitiesAndBeginBlockEntities(EntityLivingBase entity, ICamera camera, float p_147589_3_, - CallbackInfo ci) { - Shaders.endEntities(); - Shaders.beginBlockEntities(); - } - - @Inject( - method = "renderEntities(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/client/renderer/culling/ICamera;F)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/EntityRenderer;disableLightmap(D)V", - shift = At.Shift.AFTER)) - private void angelica$endBlockEntities(EntityLivingBase entity, ICamera camera, float p_147589_3_, - CallbackInfo ci) { - Shaders.endBlockEntities(); - } - - // Texture 2D - @Inject( - method = "sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", - remap = false, - ordinal = 0, - shift = At.Shift.AFTER), - expect = 1) - private void angelica$sortandRenderDisableTexture2D(EntityLivingBase p_72719_1_, int p_72719_2_, double p_72719_3_, - CallbackInfoReturnable cir) { - Shaders.disableTexture2D(); - } - - @Inject( - method = "drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", - remap = false, - ordinal = 0, - shift = At.Shift.AFTER), - expect = 1) - private void angelica$drawSelectionBoxDisableTexture2D(CallbackInfo ci) { - Shaders.disableTexture2D(); - } - - @Inject( - method = "sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", - remap = false, - ordinal = 0, - shift = At.Shift.AFTER)) - private void angelica$sortAndRenderEnableTexture2D(CallbackInfoReturnable cir) { - Shaders.enableTexture2D(); - } - - @Inject( - method = "drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", - remap = false, - ordinal = 1, - shift = At.Shift.AFTER), - expect = 1) - private void angelica$drawSelectionBoxEnableTexture2D(EntityPlayer p_72731_1_, MovingObjectPosition p_72731_2_, - int p_72731_3_, float p_72731_4_, CallbackInfo ci) { - Shaders.enableTexture2D(); - } - - // Fog - @Inject( - method = "sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", - remap = false, - ordinal = 3, - shift = At.Shift.AFTER)) - private void angelica$sortAndRenderDisableFog(CallbackInfoReturnable cir) { - Shaders.disableFog(); - } - - @Inject( - method = "sortAndRender(Lnet/minecraft/entity/EntityLivingBase;ID)I", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", - remap = false, - ordinal = 2, - shift = At.Shift.AFTER)) - private void angelica$sortAndRenderEnableFog(CallbackInfoReturnable cir) { - Shaders.enableFog(); - } - - // RenderSky - @WrapOperation( - method = "renderSky(F)V", - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", remap = false)) - private void angelica$renderSkyDisable(int cap, Operation original) { - original.call(cap); - - if (cap == GL11.GL_FOG) Shaders.disableFog(); - else if (cap == GL11.GL_TEXTURE_2D) Shaders.disableTexture2D(); - } - - @WrapOperation( - method = "renderSky(F)V", - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", remap = false)) - private void angelica$renderSkyEnable(int cap, Operation original) { - original.call(cap); - if (cap == GL11.GL_FOG) Shaders.enableFog(); - else if (cap == GL11.GL_TEXTURE_2D) Shaders.enableTexture2D(); - } - - @Inject( - method = "renderSky(F)V", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", - remap = false, - ordinal = 0, - shift = At.Shift.BEFORE, - by = 2)) - private void angelica$preSkyList(float p_72714_1_, CallbackInfo ci) { - Shaders.preSkyList(); - } - - @Inject( - method = "renderSky(F)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/multiplayer/WorldClient;getCelestialAngle(F)F", - ordinal = 1, - shift = At.Shift.BY, - by = -2)) - private void angelica$preCelestialRotate(float p_72714_1_, CallbackInfo ci) { - Shaders.preCelestialRotate(); - } - - @Inject( - method = "renderSky(F)V", - at = @At( - value = "INVOKE", - target = "Lorg/lwjgl/opengl/GL11;glRotatef(FFFF)V", - remap = false, - ordinal = 9, - shift = At.Shift.AFTER)) - private void angelica$postCelestialRotate(float p_72714_1_, CallbackInfo ci) { - Shaders.postCelestialRotate(); - } - - // drawBlockDamageTexture - - @WrapOperation( - method = "drawBlockDamageTexture(Lnet/minecraft/client/renderer/Tessellator;Lnet/minecraft/entity/EntityLivingBase;F)V", - remap = false, - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", remap = false)) - private void angelica$drawBlockDamageTextureDisable(int cap, Operation original) { - original.call(cap); - - if (cap == GL11.GL_TEXTURE_2D) Shaders.disableTexture2D(); - } - - @WrapOperation( - method = "drawBlockDamageTexture(Lnet/minecraft/client/renderer/Tessellator;Lnet/minecraft/entity/EntityLivingBase;F)V", - remap = false, - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", remap = false)) - private void angelica$drawBlockDamageTextureSkyEnable(int cap, Operation original) { - original.call(cap); - if (cap == GL11.GL_TEXTURE_2D) Shaders.enableTexture2D(); - } - - @Inject( - method = "drawBlockDamageTexture(Lnet/minecraft/client/renderer/Tessellator;Lnet/minecraft/entity/EntityLivingBase;F)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", - ordinal = 0, - shift = At.Shift.BEFORE)) - private void angelica$beginBlockDestroyProgress(Tessellator p_72717_1_, EntityLivingBase p_72717_2_, - float p_72717_3_, CallbackInfo ci) { - Shaders.beginBlockDestroyProgress(); - } - - @Inject( - method = "drawBlockDamageTexture(Lnet/minecraft/client/renderer/Tessellator;Lnet/minecraft/entity/EntityLivingBase;F)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/Tessellator;setTranslation(DDD)V", - ordinal = 1, - shift = At.Shift.AFTER)) - private void angelica$endBlockDestroyProgress(Tessellator p_72717_1_, EntityLivingBase p_72717_2_, float p_72717_3_, - CallbackInfo ci) { - Shaders.endBlockDestroyProgress(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderSpider.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderSpider.java deleted file mode 100644 index 8c26918d8..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRenderSpider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.entity.RenderSpider; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RenderSpider.class) -public class MixinRenderSpider { - - @Inject( - at = @At( - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glColor4f(FFFF)V", - value = "INVOKE"), - method = "shouldRenderPass(Lnet/minecraft/entity/monster/EntitySpider;IF)I") - private void angelica$beginSpiderEyes(CallbackInfoReturnable cir) { - Shaders.beginSpiderEyes(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRendererLivingEntity.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRendererLivingEntity.java deleted file mode 100644 index 9f891a8de..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRendererLivingEntity.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.entity.RendererLivingEntity; -import net.minecraft.entity.EntityLivingBase; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(RendererLivingEntity.class) -public abstract class MixinRendererLivingEntity { - - @Shadow - protected abstract int getColorMultiplier(EntityLivingBase p_77030_1_, float p_77030_2_, float p_77030_3_); - - @Inject(at = @At("HEAD"), method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$setEntityHurtFlash(EntityLivingBase p_76986_1_, double p_76986_2_, double p_76986_4_, - double p_76986_6_, float p_76986_8_, float p_76986_9_, CallbackInfo ci) { - if (!Shaders.useEntityHurtFlash) { - Shaders.setEntityHurtFlash( - p_76986_1_.hurtTime <= 0 && p_76986_1_.deathTime <= 0 ? 0 : 102, - this.getColorMultiplier(p_76986_1_, p_76986_1_.getBrightness(p_76986_9_), p_76986_9_)); - } - } - - @Inject( - at = @At( - target = "Lnet/minecraft/client/renderer/entity/RendererLivingEntity;renderEquippedItems(Lnet/minecraft/entity/EntityLivingBase;F)V", - value = "INVOKE"), - method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$resetEntityHurtFlash(CallbackInfo ci) { - Shaders.resetEntityHurtFlash(); - } - - @Inject( - at = @At( - ordinal = 1, - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/OpenGlHelper;setActiveTexture(I)V", - value = "INVOKE"), - method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$disableLightmap(CallbackInfo ci) { - Shaders.disableLightmap(); - } - - @Inject( - at = @At( - ordinal = 2, - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glDepthFunc(I)V", - value = "INVOKE"), - method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$beginLivingDamage(CallbackInfo ci) { - Shaders.beginLivingDamage(); - } - - @Inject( - at = @At( - ordinal = 3, - remap = false, - shift = At.Shift.AFTER, - target = "Lorg/lwjgl/opengl/GL11;glDepthFunc(I)V", - value = "INVOKE"), - method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$endLivingDamage(CallbackInfo ci) { - Shaders.endLivingDamage(); - } - - @Inject( - at = @At( - ordinal = 3, - shift = At.Shift.AFTER, - target = "Lnet/minecraft/client/renderer/OpenGlHelper;setActiveTexture(I)V", - value = "INVOKE"), - method = "doRender(Lnet/minecraft/entity/EntityLivingBase;DDDFF)V") - private void angelica$enableLightmap(CallbackInfo ci) { - Shaders.enableLightmap(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinSimpleTexture.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinSimpleTexture.java deleted file mode 100644 index ece5431e6..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinSimpleTexture.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import java.awt.image.BufferedImage; - -import net.minecraft.client.renderer.texture.AbstractTexture; -import net.minecraft.client.renderer.texture.SimpleTexture; -import net.minecraft.client.resources.IResourceManager; -import net.minecraft.util.ResourceLocation; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(value = SimpleTexture.class) -public abstract class MixinSimpleTexture extends AbstractTexture { - - private IResourceManager passedResourceManager; - - @Shadow - protected ResourceLocation textureLocation; - - // TODO: Use @Local as soon as it exists in a stable version of MixinExtras - @Inject(method = "loadTexture(Lnet/minecraft/client/resources/IResourceManager;)V", at = @At("HEAD")) - private void angelica$getResourceManager(IResourceManager p_110551_1_, CallbackInfo cbi) { - passedResourceManager = p_110551_1_; - } - - @Redirect( - method = "loadTexture(Lnet/minecraft/client/resources/IResourceManager;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureImageAllocate(ILjava/awt/image/BufferedImage;ZZ)I")) - private int angelica$loadSimpleTexture(int textureID, BufferedImage bufferedImage, boolean flag, boolean flag1) { - ShadersTex.loadSimpleTexture( - textureID, - bufferedImage, - flag, - flag1, - passedResourceManager, - this.textureLocation, - ShadersTex.getMultiTexID(this)); - return 0; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTessellator.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTessellator.java deleted file mode 100644 index 12a36bf44..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTessellator.java +++ /dev/null @@ -1,261 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.ShortBuffer; -import java.util.PriorityQueue; - -import net.minecraft.client.renderer.GLAllocation; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.shader.TesselatorVertexState; -import net.minecraft.client.util.QuadComparator; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.ShadersTess; -import com.gtnewhorizons.angelica.mixins.interfaces.TessellatorAccessor; - -@Mixin(Tessellator.class) -public abstract class MixinTessellator implements TessellatorAccessor { - - @Shadow(remap = false) - public boolean defaultTexture; - @Shadow(remap = false) - private int rawBufferSize; - @Shadow(remap = false) - public int textureID; - @Shadow - public int[] rawBuffer; - @Shadow - public int vertexCount; - @Shadow - public int rawBufferIndex; - @Shadow - public int addedVertices; - @Shadow - public boolean hasTexture; - @Shadow - public boolean hasBrightness; - @Shadow - public boolean hasNormals; - @Shadow - public boolean hasColor; - @Shadow - public double xOffset; - @Shadow - public double yOffset; - @Shadow - public double zOffset; - private ByteBuffer angelica$byteBuffer; - private IntBuffer angelica$intBuffer; - private FloatBuffer angelica$floatBuffer; - private ShortBuffer angelica$shortBuffer; - public float[] angelica$vertexPos; - public float angelica$normalX; - public float angelica$normalY; - public float angelica$normalZ; - public float angelica$midTextureU; - public float angelica$midTextureV; - - @Inject(method = "(I)V", at = @At("TAIL")) - private void angelica$extendBufferConstructor(int bufferSize, CallbackInfo ci) { - angelica$byteBuffer = GLAllocation.createDirectByteBuffer(bufferSize * 4); - angelica$intBuffer = angelica$byteBuffer.asIntBuffer(); - angelica$floatBuffer = angelica$byteBuffer.asFloatBuffer(); - angelica$shortBuffer = angelica$byteBuffer.asShortBuffer(); - rawBuffer = new int[bufferSize]; - vertexCount = 0; - angelica$vertexPos = new float[16]; - } - - @Inject(method = "()V", at = @At("TAIL")) - private void angelica$extendEmptyConstructor(CallbackInfo ci) { - this.angelica$extendBufferConstructor(65536, null); - this.defaultTexture = false; - this.rawBufferSize = 0; - this.textureID = 0; - } - - /** - * @author eigenraven - * @reason The entire drawing process must be redirected to go through an alternative renderer - */ - @Overwrite - public int draw() { - return ShadersTess.draw((Tessellator) (Object) this); - } - - /** - * @author eigenraven - * @reason byteBuffer->angelica$byteBuffer - */ - @Overwrite - public void reset() { - this.vertexCount = 0; - this.angelica$byteBuffer.clear(); - this.rawBufferIndex = 0; - this.addedVertices = 0; - } - - /** - * @author eigenraven - * @reason The entire drawing process must be redirected to go through an alternative renderer - */ - @Overwrite - public void addVertex(double x, double y, double z) { - ShadersTess.addVertex((Tessellator) (Object) this, x, y, z); - } - - /** - * @author eigenraven - * @reason Modify 32->64, ModifyConstant causes an internal mixin crash in this method - */ - @Overwrite - public TesselatorVertexState getVertexState(float x, float y, float z) { - int[] tmpCopyBuffer = new int[this.rawBufferIndex]; - @SuppressWarnings("unchecked") - PriorityQueue pQueue = new PriorityQueue<>( - this.rawBufferIndex, - new QuadComparator( - this.rawBuffer, - x + (float) this.xOffset, - y + (float) this.yOffset, - z + (float) this.zOffset)); - byte batchSize = 64; - - for (int vidx = 0; vidx < this.rawBufferIndex; vidx += batchSize) { - pQueue.add(vidx); - } - - for (int batchVidx = 0; !pQueue.isEmpty(); batchVidx += batchSize) { - int queuedVidx = pQueue.remove(); - - for (int batchElement = 0; batchElement < batchSize; ++batchElement) { - tmpCopyBuffer[batchVidx + batchElement] = this.rawBuffer[queuedVidx + batchElement]; - } - } - - System.arraycopy(tmpCopyBuffer, 0, this.rawBuffer, 0, tmpCopyBuffer.length); - return new TesselatorVertexState( - tmpCopyBuffer, - this.rawBufferIndex, - this.vertexCount, - this.hasTexture, - this.hasBrightness, - this.hasNormals, - this.hasColor); - } - - @Inject(method = "setNormal(FFF)V", at = @At("HEAD")) - private void angelica$captureNormalComponents(float x, float y, float z, CallbackInfo ci) { - this.angelica$normalX = x; - this.angelica$normalY = y; - this.angelica$normalZ = z; - } - - @Override - public ByteBuffer angelica$getByteBuffer() { - return angelica$byteBuffer; - } - - @Override - public IntBuffer angelica$getIntBuffer() { - return angelica$intBuffer; - } - - @Override - public FloatBuffer angelica$getFloatBuffer() { - return angelica$floatBuffer; - } - - @Override - public ShortBuffer angelica$getShortBuffer() { - return angelica$shortBuffer; - } - - @Override - public float[] angelica$getVertexPos() { - return angelica$vertexPos; - } - - @Override - public float angelica$getNormalX() { - return angelica$normalX; - } - - @Override - public float angelica$getNormalY() { - return angelica$normalY; - } - - @Override - public float angelica$getNormalZ() { - return angelica$normalZ; - } - - @Override - public float angelica$getMidTextureU() { - return angelica$midTextureU; - } - - @Override - public float angelica$getMidTextureV() { - return angelica$midTextureV; - } - - @Override - public void angelica$setByteBuffer(ByteBuffer angelica$byteBuffer) { - this.angelica$byteBuffer = angelica$byteBuffer; - } - - @Override - public void angelica$setIntBuffer(IntBuffer angelica$intBuffer) { - this.angelica$intBuffer = angelica$intBuffer; - } - - @Override - public void angelica$setFloatBuffer(FloatBuffer angelica$floatBuffer) { - this.angelica$floatBuffer = angelica$floatBuffer; - } - - @Override - public void angelica$setShortBuffer(ShortBuffer angelica$shortBuffer) { - this.angelica$shortBuffer = angelica$shortBuffer; - } - - @Override - public void angelica$setVertexPos(float[] angelica$vertexPos) { - this.angelica$vertexPos = angelica$vertexPos; - } - - @Override - public void angelica$setNormalX(float angelica$normalX) { - this.angelica$normalX = angelica$normalX; - } - - @Override - public void angelica$setNormalY(float angelica$normalY) { - this.angelica$normalY = angelica$normalY; - } - - @Override - public void angelica$setNormalZ(float angelica$normalZ) { - this.angelica$normalZ = angelica$normalZ; - } - - @Override - public void angelica$setMidTextureU(float angelica$midTextureU) { - this.angelica$midTextureU = angelica$midTextureU; - } - - @Override - public void angelica$setMidTextureV(float angelica$midTextureV) { - this.angelica$midTextureV = angelica$midTextureV; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureAtlasSprite.java deleted file mode 100644 index f757bd79f..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureAtlasSprite.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.TextureAtlasSprite; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(TextureAtlasSprite.class) -public class MixinTextureAtlasSprite { - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureMipmap([[IIIIIZZ)V", - value = "INVOKE"), - method = "updateAnimation()V") - private void angelica$uploadTexSub(int[][] p_147955_0_, int p_147955_1_, int p_147955_2_, int p_147955_3_, - int p_147955_4_, boolean p_147955_5_, boolean p_147955_6_) { - ShadersTex.uploadTexSub( - p_147955_0_, - p_147955_1_, - p_147955_2_, - p_147955_3_, - p_147955_4_, - p_147955_5_, - p_147955_6_); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureClock.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureClock.java deleted file mode 100644 index cc91692c2..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureClock.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.TextureClock; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(value = TextureClock.class) -public class MixinTextureClock { - - @Redirect( - method = "updateAnimation()V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureMipmap([[IIIIIZZ)V")) - private void angelica$uploadTexSub(int[][] data, int width, int height, int xoffset, int yoffset, boolean linear, - boolean clamp) { - ShadersTex.uploadTexSub(data, width, height, xoffset, yoffset, linear, clamp); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureCompass.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureCompass.java deleted file mode 100644 index b4a811e07..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureCompass.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.TextureCompass; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(value = TextureCompass.class) -public class MixinTextureCompass { - - @Redirect( - method = "updateCompass(Lnet/minecraft/world/World;DDDZZ)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureMipmap([[IIIIIZZ)V")) - private void angelica$uploadTexSub(int[][] data, int width, int height, int xoffset, int yoffset, boolean linear, - boolean clamp) { - ShadersTex.uploadTexSub(data, width, height, xoffset, yoffset, linear, clamp); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureManager.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureManager.java deleted file mode 100644 index 1ba1ff0e8..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureManager.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.texture.ITextureObject; -import net.minecraft.client.renderer.texture.TextureManager; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(TextureManager.class) -public class MixinTextureManager { - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/ITextureObject;getGlTextureId()I", - value = "INVOKE"), - method = "bindTexture(Lnet/minecraft/util/ResourceLocation;)V") - private int angelica$bindTexture(ITextureObject object) { - ShadersTex.bindTexture(object); - return 0; - } - - @Redirect( - at = @At(target = "Lnet/minecraft/client/renderer/texture/TextureUtil;bindTexture(I)V", value = "INVOKE"), - method = "bindTexture(Lnet/minecraft/util/ResourceLocation;)V") - private void angelica$noop(int p_94277_0_) { - // NO-OP - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/ITextureObject;getGlTextureId()I", - value = "INVOKE"), - method = "deleteTexture(Lnet/minecraft/util/ResourceLocation;)V") - private int angelica$deleteMultiTex(ITextureObject itextureobject) { - return ShadersTex.deleteMultiTex(itextureobject); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureMap.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureMap.java deleted file mode 100644 index c4b4457ca..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinTextureMap.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import java.io.IOException; - -import net.minecraft.client.renderer.texture.Stitcher; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.TextureMap; -import net.minecraft.client.resources.IResource; -import net.minecraft.client.resources.IResourceManager; -import net.minecraft.util.ResourceLocation; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.Slice; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import com.gtnewhorizons.angelica.client.ShadersTex; - -@Mixin(TextureMap.class) -public class MixinTextureMap extends MixinAbstractTexture { - - public int angelica$atlasWidth; - public int angelica$atlasHeight; - - @Unique - private Stitcher stitcher; - - // loadTextureAtlas - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/resources/IResourceManager;getResource(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/resources/IResource;", - value = "INVOKE"), - method = "loadTextureAtlas(Lnet/minecraft/client/resources/IResourceManager;)V") - private IResource angelica$loadResource(IResourceManager p_110571_1_, ResourceLocation resourcelocation1) - throws IOException { - return ShadersTex.loadResource(p_110571_1_, resourcelocation1); - } - - // TODO: Use @Local as soon as it is in a stable version of MixinExtras - @Inject( - at = @At(ordinal = 0, remap = false, target = "Ljava/util/Map;clear()V", value = "INVOKE"), - locals = LocalCapture.CAPTURE_FAILEXCEPTION, - method = "loadTextureAtlas(Lnet/minecraft/client/resources/IResourceManager;)V") - private void angelica$captureStitcher(IResourceManager p_110571_1_, CallbackInfo ci, int i, Stitcher stitcher) { - this.stitcher = stitcher; - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;allocateTextureImpl(IIIIF)V", - value = "INVOKE"), - method = "loadTextureAtlas(Lnet/minecraft/client/resources/IResourceManager;)V") - private void angelica$allocateTextureMap(int p_147946_0_, int p_147946_1_, int p_147946_2_, int p_147946_3_, - float p_147946_4_) { - ShadersTex.allocateTextureMap( - p_147946_0_, - p_147946_1_, - p_147946_2_, - p_147946_3_, - p_147946_4_, - this.stitcher, - (TextureMap) (Object) this); - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;getIconName()Ljava/lang/String;", - value = "INVOKE"), - method = "loadTextureAtlas(Lnet/minecraft/client/resources/IResourceManager;)V", - slice = @Slice( - from = @At( - args = "ldc=Uploading GL texture", - remap = false, - target = "Lcpw/mods/fml/common/ProgressManager$ProgressBar;step(Ljava/lang/String;)V", - value = "INVOKE_STRING"))) - private String angelica$getIconName(TextureAtlasSprite textureatlassprite) { - return ShadersTex.setIconName(ShadersTex.setSprite(textureatlassprite).getIconName()); - } - - @Redirect( - at = @At( - target = "Lnet/minecraft/client/renderer/texture/TextureUtil;uploadTextureMipmap([[IIIIIZZ)V", - value = "INVOKE"), - method = "loadTextureAtlas(Lnet/minecraft/client/resources/IResourceManager;)V") - private void angelica$uploadTexSubForLoadAtlas(int[][] p_147955_0_, int p_147955_1_, int p_147955_2_, - int p_147955_3_, int p_147955_4_, boolean p_147955_5_, boolean p_147955_6_) { - ShadersTex.uploadTexSubForLoadAtlas( - p_147955_0_, - p_147955_1_, - p_147955_2_, - p_147955_3_, - p_147955_4_, - p_147955_5_, - p_147955_6_); - } - - // updateAnimations - - @Inject(at = @At("HEAD"), method = "updateAnimations()V") - private void angelica$setUpdatingTex(CallbackInfo ci) { - ShadersTex.updatingTex = this.angelica$getMultiTexID(); - } - - @Inject(at = @At("RETURN"), method = "updateAnimations()V") - private void angelica$resetUpdatingTex(CallbackInfo ci) { - ShadersTex.updatingTex = null; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinThreadDownloadImageData.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinThreadDownloadImageData.java deleted file mode 100644 index 812194c1a..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinThreadDownloadImageData.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; - -import net.minecraft.client.renderer.ThreadDownloadImageData; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.SoftOverride; - -import com.gtnewhorizons.angelica.client.MultiTexID; - -@Mixin(ThreadDownloadImageData.class) -public abstract class MixinThreadDownloadImageData extends MixinAbstractTexture { - - @Shadow - private boolean textureUploaded; - - @Shadow - public abstract int getGlTextureId(); - - @SoftOverride - public MultiTexID angelica$getMultiTexID() { - if (!this.textureUploaded) { - this.getGlTextureId(); - } - return super.angelica$getMultiTexID(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/settings/MixinGuiVideoSettings.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/settings/MixinGuiVideoSettings.java deleted file mode 100644 index 7beb640fc..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/settings/MixinGuiVideoSettings.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.settings; - -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.GuiVideoSettings; -import net.minecraft.client.settings.GameSettings; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.GuiShaders; - -@Mixin(GuiVideoSettings.class) -public abstract class MixinGuiVideoSettings extends GuiScreen { - - private static final int SHADER_BUTTON_ID = 190; - - @Shadow - private GameSettings guiGameSettings; - - @Inject(method = "initGui()V", at = @At(value = "TAIL")) - private void angelica$addShadersButton(CallbackInfo ci) { - // Add the Shaders Button to the bottom of Video Options - final GuiButton shaderButton = new GuiButton( - SHADER_BUTTON_ID, - this.width / 2 - 190, - this.height - 27, - 70, - 20, - "Shaders..."); - this.buttonList.add(shaderButton); - } - - @Inject(method = "actionPerformed(Lnet/minecraft/client/gui/GuiButton;)V", at = @At(value = "HEAD")) - private void angelica$actionPerformed(GuiButton button, CallbackInfo ci) { - if (button.id == SHADER_BUTTON_ID) { - this.mc.gameSettings.saveOptions(); - this.mc.displayGuiScreen(new GuiShaders(this, this.guiGameSettings)); - } - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityRenderer.java new file mode 100644 index 000000000..8d243dc31 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityRenderer.java @@ -0,0 +1,101 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.pipeline.HandRenderer; +import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.uniforms.CapturedRenderingState; +import net.coderbot.iris.uniforms.SystemTimeUniforms; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.resources.IResourceManagerReloadListener; +import net.minecraft.client.settings.GameSettings; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityRenderer.class) +public abstract class MixinEntityRenderer implements IResourceManagerReloadListener { + @Shadow public Minecraft mc; + + @Inject(at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glClear(I)V", shift = At.Shift.AFTER, ordinal = 0), method = "renderWorld(FJ)V", remap = false) + private void iris$beginRender(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + CapturedRenderingState.INSTANCE.setTickDelta(partialTicks); + SystemTimeUniforms.COUNTER.beginFrame(); + SystemTimeUniforms.TIMER.beginFrame(startTime); + + Program.unbind(); + + pipeline.set(Iris.getPipelineManager().preparePipeline(Iris.getCurrentDimension())); + + pipeline.get().beginLevelRendering(); + } + + @Inject(method = "renderWorld(FJ)V", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) + private void iris$endLevelRender(float partialTicks, long limitTime, CallbackInfo callback, @Share("pipeline") LocalRef pipeline) { + // TODO: Iris + final Camera camera = new Camera(mc.renderViewEntity, partialTicks); + HandRenderer.INSTANCE.renderTranslucent(null /*poseStack*/, partialTicks, camera, mc.renderGlobal, pipeline.get()); + Minecraft.getMinecraft().mcProfiler.endStartSection("iris_final"); + pipeline.get().finalizeLevelRendering(); + pipeline.set(null); + Program.unbind(); + } + + @Inject(at = @At(value= "INVOKE", target="Lnet/minecraft/client/renderer/RenderGlobal;clipRenderersByFrustum(Lnet/minecraft/client/renderer/culling/ICamera;F)V", shift=At.Shift.AFTER), method = "renderWorld(FJ)V") + private void iris$beginEntities(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + final Camera camera = new Camera(mc.renderViewEntity, partialTicks); + pipeline.get().renderShadows((EntityRenderer) (Object) this, camera); + } + + + @Redirect(method = "renderWorld(FJ)V", at = @At(value = "FIELD", target = "Lnet/minecraft/client/settings/GameSettings;renderDistanceChunks:I") ) + /*slice = @Slice(from = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"))*/ + private int iris$alwaysRenderSky(GameSettings instance) { + return Math.max(instance.renderDistanceChunks, 4); + } + @Inject(method = "renderWorld(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(F)V")) + private void iris$beginSky(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + // Use CUSTOM_SKY until levelFogColor is called as a heuristic to catch FabricSkyboxes. + pipeline.get().setPhase(WorldRenderingPhase.CUSTOM_SKY); + } + + + @Inject(method = "renderWorld(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(F)V", shift = At.Shift.AFTER)) + private void iris$endSky(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.NONE); + } + + @WrapOperation(method = "renderWorld(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/EntityRenderer;renderCloudsCheck(Lnet/minecraft/client/renderer/RenderGlobal;F)V")) + private void iris$clouds(EntityRenderer instance, RenderGlobal rg, float partialTicks, Operation original, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.CLOUDS); + original.call(instance, rg, partialTicks); + pipeline.get().setPhase(WorldRenderingPhase.NONE); + } + + + @Inject(method = "renderWorld(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/EntityRenderer;renderRainSnow(F)V")) + private void iris$beginWeatherAndwriteRainAndSnowToDepthBuffer(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.RAIN_SNOW); + if (pipeline.get().shouldWriteRainAndSnowToDepthBuffer()) { + GL11.glDepthMask(true); + } + } + + @Inject(method = "renderWorld(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/EntityRenderer;renderRainSnow(F)V", shift = At.Shift.AFTER)) + private void iris$endWeather(float partialTicks, long startTime, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.NONE); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinFramebuffer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinFramebuffer.java new file mode 100644 index 000000000..5d7027f6d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinFramebuffer.java @@ -0,0 +1,86 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; +import net.coderbot.iris.rendertarget.IRenderTargetExt; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL30; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.nio.IntBuffer; + +@Mixin(Framebuffer.class) +public abstract class MixinFramebuffer implements IRenderTargetExt { + private int iris$depthBufferVersion; + + private int iris$colorBufferVersion; + + @Getter public boolean iris$useDepth; + @Getter public int iris$depthTextureId = -1; + + @Shadow public boolean useDepth; + + + @Inject(method = "deleteFramebuffer()V", at = @At(value="INVOKE", target="Lnet/minecraft/client/shader/Framebuffer;unbindFramebuffer()V", shift = At.Shift.AFTER)) + private void iris$onDestroyBuffers(CallbackInfo ci) { + iris$depthBufferVersion++; + iris$colorBufferVersion++; + } + + @Override + public int iris$getDepthBufferVersion() { + return iris$depthBufferVersion; + } + + @Override + public int iris$getColorBufferVersion() { + return iris$colorBufferVersion; + } + + // Use a depth texture instead of a depth drawScreen buffer + @Inject(method="Lnet/minecraft/client/shader/Framebuffer;createBindFramebuffer(II)V", at=@At(value="HEAD")) + private void iris$useDepthTexture(int width, int height, CallbackInfo ci) { + if(this.useDepth) { + this.useDepth = false; + this.iris$useDepth = true; + } + } + + @Inject(method="deleteFramebuffer()V", at=@At(value="FIELD", target="Lnet/minecraft/client/shader/Framebuffer;depthBuffer:I", shift = At.Shift.BEFORE, ordinal = 0), remap = false) + private void iris$deleteDepthBuffer(CallbackInfo ci) { + if(this.iris$depthTextureId > -1 ) { + GLStateManager.glDeleteTextures(this.iris$depthTextureId); + this.iris$depthTextureId = -1; + } + } + + @Inject(method="createFramebuffer(II)V", at=@At(value="FIELD", target="Lnet/minecraft/client/shader/Framebuffer;useDepth:Z", shift=At.Shift.BEFORE, ordinal = 0)) + private void iris$createDepthTextureID(int width, int height, CallbackInfo ci) { + if (this.iris$useDepth) { + this.iris$depthTextureId = GL11.glGenTextures(); + } + } + + @Inject(method="createFramebuffer(II)V", at=@At(value="FIELD", target="Lnet/minecraft/client/shader/Framebuffer;useDepth:Z", shift=At.Shift.BEFORE, ordinal = 1)) + private void iris$createDepthTexture(int width, int height, CallbackInfo ci) { + if(this.iris$useDepth) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, this.iris$depthTextureId); + + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_COMPARE_MODE, 0); + GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_DEPTH_COMPONENT, width, height, 0, GL11.GL_DEPTH_COMPONENT, GL11.GL_FLOAT, (IntBuffer) null); + OpenGlHelper.func_153188_a/*glFramebufferTexture2D*/(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL11.GL_TEXTURE_2D, this.iris$depthTextureId, 0); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItem.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItem.java new file mode 100644 index 000000000..e972eebe1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItem.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import net.irisshaders.iris.api.v0.item.IrisItemLightProvider; +import net.minecraft.item.Item; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Item.class) +public class MixinItem implements IrisItemLightProvider { + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinLocale.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinLocale.java new file mode 100644 index 000000000..eab97ec11 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinLocale.java @@ -0,0 +1,82 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.LanguageMap; +import net.coderbot.iris.shaderpack.ShaderPack; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.Locale; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@Mixin(Locale.class) +public class MixinLocale { + // storage + @Shadow Map field_135032_a; + + @Unique + private static final List languageCodes = new ArrayList<>(); + + @Inject(method = "translateKeyPrivate", at = @At("HEAD"), cancellable = true) + private void iris$overrideLanguageEntries(String key, CallbackInfoReturnable cir) { + final String override = iris$lookupOverriddenEntry(key); + + if (override != null) { + cir.setReturnValue(override); + } + } + + @Unique + private String iris$lookupOverriddenEntry(String key) { + final ShaderPack pack = Iris.getCurrentPack().orElse(null); + + if (pack == null) { + // If no shaderpack is loaded, do not try to process language overrides. + // + // This prevents a cryptic NullPointerException when shaderpack loading fails for some reason. + return null; + } + + // Minecraft loads the "en_US" language code by default, and any other code will be right after it. + // + // So we also check if the user is loading a special language, and if the shaderpack has support for that + // language. If they do, we load that, but if they do not, we load "en_US" instead. + final LanguageMap languageMap = pack.getLanguageMap(); + + if (field_135032_a.containsKey(key)) { + return null; + } + + for (String code : languageCodes) { + final Map translations = languageMap.getTranslations(code); + + if (translations != null) { + final String translation = translations.get(key); + + if (translation != null) { + return translation; + } + } + } + + return null; + } + + @Inject(method = "loadLocaleDataFiles", at = @At("HEAD")) + private void iris$addLanguageCodes(IResourceManager resourceManager, List definitions, CallbackInfo ci) { + languageCodes.clear(); + + // Reverse order due to how minecraft has English and then the primary language in the language definitions list + new LinkedList<>(definitions).descendingIterator().forEachRemaining(languageCodes::add); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinMinecraft.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinMinecraft.java deleted file mode 100644 index 8d80674f3..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinMinecraft.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.shaders; - -import net.minecraft.client.Minecraft; -import net.minecraft.profiler.IPlayerUsage; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.Shaders; - -@Mixin(Minecraft.class) -public abstract class MixinMinecraft implements IPlayerUsage { - - @Inject( - method = "startGame()V", - at = @At( - ordinal = 1, - shift = At.Shift.AFTER, - value = "INVOKE", - target = "Lnet/minecraft/client/Minecraft;checkGLError(Ljava/lang/String;)V")) - private void angelica$Startup(CallbackInfo ci) { - // Start the Shaders - Shaders.startup(((Minecraft) ((Object) this))); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinOpenGlHelper.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinOpenGlHelper.java new file mode 100644 index 000000000..aff6ac976 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinOpenGlHelper.java @@ -0,0 +1,53 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import net.minecraft.client.renderer.OpenGlHelper; +import org.lwjgl.opengl.GL30; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(OpenGlHelper.class) +public class MixinOpenGlHelper { + @Unique private static int iris$drawFramebuffer = 0; + @Unique private static int iris$readFramebuffer = 0; + + @Inject(method="func_153171_g", at=@At("HEAD"), cancellable=true) + private static void iris$avoidRedundantBind(int target, int framebuffer, CallbackInfo ci) { + if (target == GL30.GL_FRAMEBUFFER) { + if (iris$drawFramebuffer == target && iris$readFramebuffer == target) { + ci.cancel(); + } else { + iris$drawFramebuffer = framebuffer; + iris$readFramebuffer = framebuffer; + } + } else if (target == GL30.GL_DRAW_FRAMEBUFFER) { + if (iris$drawFramebuffer == target) { + ci.cancel(); + } else { + iris$drawFramebuffer = framebuffer; + } + } else if (target == GL30.GL_READ_FRAMEBUFFER) { + if (iris$readFramebuffer == target) { + ci.cancel(); + } else { + iris$readFramebuffer = framebuffer; + } + } else { + throw new IllegalStateException("Invalid framebuffer target: " + target); + } + } + + @Inject(method="func_153174_h", at=@At("HEAD")) + private static void iris$trackFramebufferDelete(int framebuffer, CallbackInfo ci) { + if (iris$drawFramebuffer == framebuffer) { + iris$drawFramebuffer = 0; + } + + if (iris$readFramebuffer == framebuffer) { + iris$readFramebuffer = 0; + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRender.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRender.java similarity index 58% rename from src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRender.java rename to src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRender.java index be2289ff4..c1af075a1 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/early/renderer/MixinRender.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRender.java @@ -1,19 +1,22 @@ -package com.gtnewhorizons.angelica.mixins.early.renderer; +package com.gtnewhorizons.angelica.mixins.early.shaders; +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.minecraft.client.renderer.entity.Render; - import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import com.gtnewhorizons.angelica.client.Shaders; - @Mixin(Render.class) public class MixinRender { - @Inject(method = "renderShadow(Lnet/minecraft/entity/Entity;DDDFF)V", at = @At("HEAD"), cancellable = true) private void angelica$checkShouldSkipDefaultShadow(CallbackInfo ci) { - if (Shaders.shouldSkipDefaultShadow) ci.cancel(); + final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + + if (pipeline != null && pipeline.shouldDisableVanillaEntityShadows()) { + ci.cancel(); + } } + } diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java new file mode 100644 index 000000000..4b884e52c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java @@ -0,0 +1,56 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.minecraft.client.renderer.RenderGlobal; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Slice; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderGlobal.class) +public class MixinRenderGlobal { + @Inject(method = "renderSky", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/Tessellator;instance:Lnet/minecraft/client/renderer/Tessellator;")) + private void iris$renderSky$beginNormalSky(float partialTicks, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + // None of the vanilla sky is rendered until after this call, so if anything is rendered before, it's CUSTOM_SKY. + pipeline.set(Iris.getPipelineManager().getPipelineNullable()); + pipeline.get().setPhase(WorldRenderingPhase.SKY); + } + + @Inject(method = "renderSky", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/RenderGlobal;locationSunPng:Lnet/minecraft/util/ResourceLocation;")) + private void iris$setSunRenderStage(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.SUN); + } + + @Inject(method = "renderSky", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/RenderGlobal;locationMoonPhasesPng:Lnet/minecraft/util/ResourceLocation;")) + private void iris$setMoonRenderStage(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.MOON); + } + + @Inject(method = "renderSky", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/WorldProvider;calcSunriseSunsetColors(FF)[F")) + private void iris$setSunsetRenderStage(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.SUNSET); + } + + @Inject(method = "renderSky", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/WorldClient;getStarBrightness(F)F")) + private void iris$setStarRenderStage(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.STARS); + } + + @Inject(method = "renderSky", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/EntityClientPlayerMP;getPosition(F)Lnet/minecraft/util/Vec3;")) + private void iris$setVoidRenderStage(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + pipeline.get().setPhase(WorldRenderingPhase.VOID); + } + + @Inject(method = "renderSky", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/WorldClient;getCelestialAngle(F)F"), + slice = @Slice(from = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/WorldClient;getRainStrength(F)F"))) + private void iris$renderSky$tiltSun(float p_72714_1_, CallbackInfo ci, @Share("pipeline") LocalRef pipeline) { + GL11.glRotatef(pipeline.get().getSunPathRotation(), 0.0F, 0.0F, 1.0F); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/AnimationMetadataSectionAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/AnimationMetadataSectionAccessor.java new file mode 100644 index 000000000..83e260c16 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/AnimationMetadataSectionAccessor.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AnimationMetadataSection.class) +public interface AnimationMetadataSectionAccessor { + @Accessor("frameWidth") + int getFrameWidth(); + + @Mutable + @Accessor("frameWidth") + void setFrameWidth(int frameWidth); + + @Accessor("frameHeight") + int getFrameHeight(); + + @Mutable + @Accessor("frameHeight") + void setFrameHeight(int frameHeight); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/EntityRendererAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/EntityRendererAccessor.java new file mode 100644 index 000000000..1dbab0d41 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/EntityRendererAccessor.java @@ -0,0 +1,13 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.entity.player.EntityPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(EntityRenderer.class) +public interface EntityRendererAccessor { + @Invoker + float invokeGetNightVisionBrightness(EntityPlayer entityPlayer, float partialTicks); + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/MinecraftAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/MinecraftAccessor.java new file mode 100644 index 000000000..767f185af --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/MinecraftAccessor.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.Minecraft; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import net.minecraft.util.Timer; + +@Mixin(Minecraft.class) +public interface MinecraftAccessor { + @Accessor("timer") + Timer getTimer(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/SimpleTextureAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/SimpleTextureAccessor.java new file mode 100644 index 000000000..bc346ff60 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/SimpleTextureAccessor.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.renderer.texture.SimpleTexture; +import net.minecraft.util.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SimpleTexture.class) +public interface SimpleTextureAccessor { + @Accessor("textureLocation") + ResourceLocation getLocation(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureAtlasSpriteAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureAtlasSpriteAccessor.java new file mode 100644 index 000000000..75315a593 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureAtlasSpriteAccessor.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; + +@Mixin(TextureAtlasSprite.class) +public interface TextureAtlasSpriteAccessor { + @Accessor("animationMetadata") + AnimationMetadataSection getMetadata(); + + @Accessor("framesTextureData") + List getFramesTextureData(); + + @Accessor("frameCounter") + int getFrame(); + + @Accessor("frameCounter") + void setFrame(int frame); + + @Accessor("tickCounter") + int getSubFrame(); + + @Accessor("tickCounter") + void setSubFrame(int subFrame); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureMapAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureMapAccessor.java new file mode 100644 index 000000000..1e34f880c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/accessors/TextureMapAccessor.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.accessors; + +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.util.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(TextureMap.class) +public interface TextureMapAccessor { + + @Accessor("mapUploadedSprites") + Map getMapUploadedSprites(); + + @Accessor("anisotropicFiltering") + int getAnisotropicFiltering(); + + @Accessor("mipmapLevels") + int getMipmapLevels(); + + @Accessor("locationBlocksTexture") + ResourceLocation getLocationBlocksTexture(); + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinAbstractTexture.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinAbstractTexture.java new file mode 100644 index 000000000..2c7d42093 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinAbstractTexture.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.startup; + +import net.coderbot.iris.texture.TextureTracker; +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(AbstractTexture.class) +public class MixinAbstractTexture { + @Shadow + public int glTextureId; + + // Inject after the newly-generated texture ID has been stored into the id field + @Inject(method = "getGlTextureId()I", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/TextureUtil;glGenTextures()I", shift = At.Shift.BY, by = 2)) + private void iris$afterGenerateId(CallbackInfoReturnable cir) { + TextureTracker.INSTANCE.trackTexture(glTextureId, (AbstractTexture) (Object) this); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGameSettings.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGameSettings.java new file mode 100644 index 000000000..3eb97abd9 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGameSettings.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.startup; + +import net.coderbot.iris.Iris; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GameSettings.class) +public abstract class MixinGameSettings { + @Unique + private static boolean iris$shadersInitialized; + + @Inject(method="Lnet/minecraft/client/settings/GameSettings;loadOptions()V", at=@At("HEAD")) + private void angelica$InitializeShaders(CallbackInfo ci) { + if (iris$shadersInitialized) { + return; + } + + iris$shadersInitialized = true; + Iris.INSTANCE.onEarlyInitialize(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGuiMainMenu.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGuiMainMenu.java new file mode 100644 index 000000000..407bc8f68 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinGuiMainMenu.java @@ -0,0 +1,18 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.startup; + +import net.coderbot.iris.Iris; +import net.minecraft.client.gui.GuiMainMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiMainMenu.class) +public class MixinGuiMainMenu { + + @Inject(method = "initGui", at = @At("RETURN")) + public void angelica$shadersOnLoadingComplete(CallbackInfo ci) { + Iris.onLoadingComplete(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinInitRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinInitRenderer.java new file mode 100644 index 000000000..8b5f2ef35 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinInitRenderer.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.startup; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.GLDebug; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.minecraft.client.renderer.OpenGlHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(OpenGlHelper.class) +public class MixinInitRenderer { + @Inject(method = "initializeTextures", at = @At("RETURN")) + private static void angelica$initializeRenderer(CallbackInfo ci) { + if (Thread.currentThread() != GLStateManager.getMainThread()) { + AngelicaTweaker.LOGGER.warn("Renderer initialization called from non-main thread!"); + return; + } + + Iris.identifyCapabilities(); + GLDebug.initRenderer(); + IrisRenderSystem.initRenderer(); + Iris.onRenderSystemInit(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinTextureAtlasSprite.java new file mode 100644 index 000000000..f7fd38865 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/shaders/startup/MixinTextureAtlasSprite.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders.startup; + +import net.coderbot.iris.texture.pbr.PBRSpriteHolder; +import net.coderbot.iris.texture.pbr.TextureAtlasSpriteExtension; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(TextureAtlasSprite.class) +public class MixinTextureAtlasSprite implements TextureAtlasSpriteExtension { + @Unique + private PBRSpriteHolder pbrHolder; + + @Override + public PBRSpriteHolder getPBRHolder() { + return pbrHolder; + } + + @Override + public PBRSpriteHolder getOrCreatePBRHolder() { + if (pbrHolder == null) { + pbrHolder = new PBRSpriteHolder(); + } + return pbrHolder; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlock.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlock.java new file mode 100644 index 000000000..a0929c288 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlock.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.glsm.ThreadedBlockData; +import net.minecraft.block.Block; +import org.spongepowered.asm.mixin.Mixin; + +/** + * Store thread-safe block data here. + * + * We need to be careful - blocks will initialize some stuff in the constructor, so the first ThreadedBlockData + * instance should be used as the clone for all others. This will ensure the block bounds are set correctly + * for blocks that don't change their bounds at runtime. + */ +@Mixin(Block.class) +public class MixinBlock implements ThreadedBlockData.Getter { + private final ThreadLocal angelica$threadData = ThreadLocal.withInitial(() -> null); + private volatile ThreadedBlockData angelica$initialData; + + @Override + public ThreadedBlockData angelica$getThreadData() { + ThreadedBlockData data = angelica$threadData.get(); + if(data != null) + return data; + + return createThreadedBlockData(); + } + + private ThreadedBlockData createThreadedBlockData() { + ThreadedBlockData data; + + synchronized (this) { + if(angelica$initialData == null) { + data = angelica$initialData = new ThreadedBlockData(); + } else { + data = new ThreadedBlockData(angelica$initialData); + } + } + + angelica$threadData.set(data); + + return data; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlockLiquid.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlockLiquid.java new file mode 100644 index 000000000..c7baa4eac --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinBlockLiquid.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockLiquid; +import net.minecraft.block.material.Material; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidRegistry; +import net.minecraftforge.fluids.IFluidBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(BlockLiquid.class) +public abstract class MixinBlockLiquid implements IFluidBlock { + + @Shadow + public abstract int getBlockColor(); + + @Override + public Fluid getFluid() { + return ((Block) (Object) this).getMaterial() == Material.water ? FluidRegistry.WATER : FluidRegistry.LAVA; + } + + @Override + public float getFilledPercentage(World world, int x, int y, int z) { + return getFluid() == null ? 0 : 1 - BlockLiquid.getLiquidHeightPercent(world.getBlockMetadata(x, y, z)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunk.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunk.java new file mode 100644 index 000000000..a24700e24 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunk.java @@ -0,0 +1,45 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.World; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.biome.WorldChunkManager; +import net.minecraft.world.chunk.Chunk; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Chunk.class) +public abstract class MixinChunk { + @Shadow + public World worldObj; + + @Shadow + private byte[] blockBiomeArray; + + @Shadow + @Final + public int xPosition, zPosition; + + @Inject(method = "fillChunk", at = @At("RETURN")) + private void sodium$populateBiomes(CallbackInfo ci) { + if(this.worldObj.isRemote && !Minecraft.getMinecraft().isSingleplayer()) { + // We are in multiplayer, the server might not have sent all biomes to the client. + // Populate them now while we're on the main thread. + WorldChunkManager manager = this.worldObj.getWorldChunkManager(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + int idx = (z << 4) + x; + int biome = this.blockBiomeArray[idx] & 255; + if(biome == 255) { + BiomeGenBase generated = manager.getBiomeGenAt((this.xPosition << 4) + x, (this.zPosition << 4) + z); + this.blockBiomeArray[idx] = (byte)(generated.biomeID & 255); + } + } + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderClient.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderClient.java new file mode 100644 index 000000000..afdaa4399 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderClient.java @@ -0,0 +1,18 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import net.minecraft.client.multiplayer.ChunkProviderClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ChunkProviderClient.class) +public abstract class MixinChunkProviderClient { + + @Inject(method="unloadChunk", at=@At("TAIL")) + private void sodium$unloadChunk(int x, int z, CallbackInfo ci) { + SodiumWorldRenderer.getInstance().onChunkRemoved(x, z); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderServer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderServer.java new file mode 100644 index 000000000..83edd75c6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinChunkProviderServer.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.world.biome.WorldChunkManager; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.gen.ChunkProviderServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ChunkProviderServer.class) +public class MixinChunkProviderServer { + @Redirect(method = "originalLoadChunk", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/IChunkProvider;provideChunk(II)Lnet/minecraft/world/chunk/Chunk;", remap = true), remap = false) + private Chunk sodium$populateChunkWithBiomes(IChunkProvider instance, int chunkX, int chunkZ) { + Chunk chunk = instance.provideChunk(chunkX, chunkZ); + if(chunk != null) { + WorldChunkManager manager = chunk.worldObj.getWorldChunkManager(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + chunk.getBiomeGenForWorldCoords(x, z, manager); + } + } + } + return chunk; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEffectRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEffectRenderer.java new file mode 100644 index 000000000..78859e1ef --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEffectRenderer.java @@ -0,0 +1,59 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EffectRenderer.class) +public class MixinEffectRenderer { + + @Unique + private Frustrum cullingFrustum; + + @Unique + private void setupCullingFrustum() { + Frustrum frustum = SodiumWorldRenderer.getInstance().getFrustum(); + boolean useCulling = SodiumClientMod.options().advanced.useParticleCulling; + // Setup the frustum state before rendering particles + if (useCulling && frustum != null) { + this.cullingFrustum = frustum; + } else { + this.cullingFrustum = null; + } + } + + @Inject(method = "renderParticles", at = @At("HEAD")) + private void setupFrustum$standart(Entity player, float partialTickTime, CallbackInfo ci) { + setupCullingFrustum(); + } + + @Redirect(method = "renderParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;renderParticle(Lnet/minecraft/client/renderer/Tessellator;FFFFFF)V")) + private void renderParticles(EntityFX particle, Tessellator tessellator, float partialTicks, float rotationX, float rotationZ, float rotationYZ, float rotationXY, float rotationXZ) { + if(cullingFrustum == null || cullingFrustum.isBoundingBoxInFrustum(particle.boundingBox)) { + particle.renderParticle(tessellator, partialTicks, rotationX, rotationZ, rotationYZ, rotationXY, rotationXZ); + } + } + + @Inject(method = "renderLitParticles", at = @At("HEAD")) + private void setupFrustum$lit(Entity player, float partialTickTime, CallbackInfo ci) { + setupCullingFrustum(); + } + + @Redirect(method = "renderLitParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;renderParticle(Lnet/minecraft/client/renderer/Tessellator;FFFFFF)V")) + private void renderLitParticles(EntityFX particle, Tessellator tessellator, float partialTicks, float rotationX, float rotationZ, float rotationYZ, float rotationXY, float rotationXZ) { + if(cullingFrustum == null || cullingFrustum.isBoundingBoxInFrustum(particle.boundingBox)) { + particle.renderParticle(tessellator, partialTicks, rotationX, rotationZ, rotationYZ, rotationXY, rotationXZ); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntity.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntity.java new file mode 100644 index 000000000..4cbc969a9 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntity.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.llamalad7.mixinextras.sugar.Local; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public abstract class MixinEntity { + + @Shadow + public abstract boolean isInRangeToRenderDist(double dist); + + @Inject(method ="isInRangeToRender3d", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isInRangeToRenderDist(D)Z"), cancellable = true) + private void sodium$afterDistCalc(CallbackInfoReturnable ci, @Local(ordinal = 6) double d6) { + ci.setReturnValue(this.isInRangeToRenderDist(d6/(SodiumGameOptions.EntityRenderDistance.entityRenderDistanceMultiplier * SodiumGameOptions.EntityRenderDistance.entityRenderDistanceMultiplier))); + } + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityFX.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityFX.java new file mode 100644 index 000000000..9e0a4498a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityFX.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import me.jellysquid.mods.sodium.client.render.texture.SpriteUtil; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.util.IIcon; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityFX.class) +public class MixinEntityFX { + + @Shadow + protected IIcon particleIcon; + + private boolean shouldTickSprite; + + @Inject(method = "setParticleIcon", at = @At("RETURN")) + public void afterSetSprite(IIcon icon, CallbackInfo ci) { + this.shouldTickSprite = icon instanceof ISpriteExt atlasSprite && atlasSprite.isAnimation(); + } + + @Inject(method = "renderParticle", at = @At("HEAD")) + public void renderParticle(Tessellator tessellator, float offset, float x, float y, float z, float u, float v, CallbackInfo ci) { + if (this.shouldTickSprite && this.particleIcon instanceof TextureAtlasSprite atlasSprite) { + SpriteUtil.markSpriteActive(atlasSprite); + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityRenderer.java new file mode 100644 index 000000000..a3e72c685 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinEntityRenderer.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.compat.FogHelper; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityRenderer.class) +public class MixinEntityRenderer { + @Shadow + float fogColorRed, fogColorGreen, fogColorBlue; + @Inject(method = "updateFogColor", at = @At("RETURN")) + private void storeFog(CallbackInfo ci) { + FogHelper.red = fogColorRed; + FogHelper.green = fogColorGreen; + FogHelper.blue = fogColorBlue; + } + + @Redirect(method = "renderRainSnow(F)V", at = @At(value = "FIELD", target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", opcode = Opcodes.GETFIELD)) + protected boolean redirectGetFancyWeather(GameSettings settings) { + return SodiumClientMod.options().quality.weatherQuality.isFancy(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinExtendedBlockStorage.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinExtendedBlockStorage.java new file mode 100644 index 000000000..e444e7843 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinExtendedBlockStorage.java @@ -0,0 +1,16 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ExtendedBlockStorage.class) +public interface MixinExtendedBlockStorage { + + @Accessor("yBase") + int getYBase(); + @Accessor("blockRefCount") + int getBlockRefCount(); + @Accessor("blockRefCount") + void setBlockRefCount(int blockRefCount); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFMLClientHandler.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFMLClientHandler.java new file mode 100644 index 000000000..3abf6ad7f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFMLClientHandler.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.Tags; +import cpw.mods.fml.client.FMLClientHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import java.util.Collections; +import java.util.List; + +@Mixin(value=FMLClientHandler.class, remap=false) +public class MixinFMLClientHandler { + /** + * @author mitchej123 + * @reason Hack in Additional FML Branding + */ + @Overwrite + public List getAdditionalBrandingInformation() { + return Collections.singletonList(String.format("Angelica %s", Tags.VERSION)); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFrustrum.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFrustrum.java new file mode 100644 index 000000000..fc4bdf3c3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinFrustrum.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import net.minecraft.client.renderer.culling.Frustrum; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(Frustrum.class) +public abstract class MixinFrustrum implements FrustumExtended { + @Shadow + public abstract boolean isBoxInFrustum(double p_78548_1_, double p_78548_3_, double p_78548_5_, double p_78548_7_, double p_78548_9_, double p_78548_11_); + + @Override + public boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return isBoxInFrustum(minX, minY, minZ, maxX, maxY, maxZ); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGameSettings.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGameSettings.java new file mode 100644 index 000000000..c948017e4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGameSettings.java @@ -0,0 +1,18 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(GameSettings.class) +public class MixinGameSettings { + /** + * @author embeddedt + * @reason Sodium Renderer supports up to 32 chunks + */ + @ModifyConstant(method = "(Lnet/minecraft/client/Minecraft;Ljava/io/File;)V", constant = @Constant(floatValue = 16.0f)) + private float increaseMaxDistance(float old) { + return 32.0f; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGuiIngameForge.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGuiIngameForge.java new file mode 100644 index 000000000..7715670f2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinGuiIngameForge.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraftforge.client.GuiIngameForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(GuiIngameForge.class) +public class MixinGuiIngameForge { + + @Redirect(method = "renderGameOverlay(FZII)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isFancyGraphicsEnabled()Z")) + private boolean checkVignette(float idk) { + return SodiumClientMod.options().quality.enableVignette.isFancy(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinLongHashMap.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinLongHashMap.java new file mode 100644 index 000000000..136bd27ff --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinLongHashMap.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.util.LongHashMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(LongHashMap.class) +public class MixinLongHashMap { + /** + * @author TheMasterCaver, embeddedt (mixin version) + * @reason Use a better hash (from TMCW) that avoids collisions. + */ + @Overwrite + private static int getHashedKey(long par0) { + return (int)par0 + (int)(par0 >>> 32) * 92821; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMaterial.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMaterial.java new file mode 100644 index 000000000..0f0b1dc94 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMaterial.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.block.material.Material; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Material.class) +public interface MixinMaterial { + + @Accessor("isTranslucent") + boolean getIsTranslucent(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMinecraft.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMinecraft.java new file mode 100644 index 000000000..3be2c920c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinMinecraft.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.mixins.interfaces.IRenderGlobalExt; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public class MixinMinecraft { + @Shadow + public RenderGlobal renderGlobal; + + @Inject(method="resize", at=@At("TAIL")) + private void sodium$resize(int width, int height, CallbackInfo ci) { + if(this.renderGlobal != null) { + ((IRenderGlobalExt)this.renderGlobal).scheduleTerrainUpdate(); + } + } + + @Redirect(method="runGameLoop", at=@At(value="FIELD", target="Lnet/minecraft/client/renderer/WorldRenderer;chunksUpdated:I", ordinal=0)) + private int sodium$chunksUpdated() { + return ((IRenderGlobalExt)this.renderGlobal).getChunksSubmitted(); + } + + @Redirect(method = "runGameLoop", at = @At(value = "FIELD", target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z")) + private boolean sodium$overrideFancyGrass(GameSettings gameSettings) { + return SodiumClientMod.options().quality.grassQuality.isFancy(); + } + + @Inject(method = "checkGLError", at = @At(value = "HEAD"), cancellable = true) + private void sodium$checkGLError(CallbackInfo ci) { + if(SodiumClientMod.options().performance.useNoErrorGLContext) + ci.cancel(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinNibbleArray.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinNibbleArray.java new file mode 100644 index 000000000..dcc980a46 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinNibbleArray.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.mixins.interfaces.ExtendedNibbleArray; +import net.minecraft.world.chunk.NibbleArray; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(NibbleArray.class) +public abstract class MixinNibbleArray implements ExtendedNibbleArray { + @Shadow public byte[] data; + @Shadow private int depthBits; + @Shadow private int depthBitsPlusFour; + + @Override + public byte[] getData() { + return data; + } + + @Override + public int getDepthBits() { + return depthBits; + } + + @Override + public int getDepthBitsPlusFour() { + return depthBitsPlusFour; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRender.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRender.java new file mode 100644 index 000000000..80be62237 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRender.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.settings.GameSettings; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Render.class) +public class MixinRender { + + @Redirect(method = "doRenderShadowAndFire", at = @At(value = "FIELD", target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", opcode = Opcodes.GETFIELD, ordinal = 0)) + private boolean redirectGetFancyShadows(GameSettings settings) { + return SodiumClientMod.options().quality.entityShadows.isFancy(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java new file mode 100644 index 000000000..aaa3df878 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderBlocks; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(RenderBlocks.class) +public abstract class MixinRenderBlocks { + @Redirect(method = "renderStandardBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isAmbientOcclusionEnabled()Z")) + private boolean checkAOEnabled() { + if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo()) { + return false; // force Sodium pipeline + } + + return Minecraft.isAmbientOcclusionEnabled(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java new file mode 100644 index 000000000..6d617dd4f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java @@ -0,0 +1,309 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.mixins.interfaces.IRenderGlobalExt; +import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; +import com.gtnewhorizons.angelica.rendering.RenderingState; + +import lombok.Getter; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.HandRenderer; +import net.coderbot.iris.pipeline.ShadowRenderer; +import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerControllerMP; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.init.Blocks; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static org.joml.Math.lerp; + +// Let other mixins apply, and then overwrite them +@Mixin(value = RenderGlobal.class, priority = 2000) +public class MixinRenderGlobal implements IRenderGlobalExt { + @Shadow public int renderChunksWide; + @Shadow public int renderChunksTall; + @Shadow public int renderChunksDeep; + @Shadow public WorldClient theWorld; + @Shadow public Minecraft mc; + @Shadow public int renderDistanceChunks; + @Shadow public WorldRenderer[] worldRenderers; + @Shadow public WorldRenderer[] sortedWorldRenderers; + + @Getter + @Unique private SodiumWorldRenderer renderer; + + private int sodium$frame; + + @Override + public void scheduleTerrainUpdate() { + this.renderer.scheduleTerrainUpdate(); + } + + @Override + public int getChunksSubmitted() { + return this.renderer.getChunksSubmitted(); + } + + @Inject(method="", at=@At("RETURN")) + private void sodium$initRenderer(Minecraft mc, CallbackInfo ci) { + this.renderer = SodiumWorldRenderer.create(mc); + } + + @Inject(method="Lnet/minecraft/client/renderer/RenderGlobal;setWorldAndLoadRenderers(Lnet/minecraft/client/multiplayer/WorldClient;)V", at=@At("RETURN")) + private void sodium$setWorldAndLoadRenderers(WorldClient world, CallbackInfo ci) { + RenderDevice.enterManagedCode(); + try { + this.renderer.setWorld(world); + } finally { + RenderDevice.exitManagedCode(); + } + } + + /** + * @author mitchej123, sodium + * @reason Redirect to our renderer + */ + @Overwrite + public String getDebugInfoRenders() { + return this.renderer.getChunksDebugString(); + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public int renderSortedRenderers(int x, int z, int pass, double partialTicks) { + // Do nothing + return 0; + } + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void renderAllRenderLists(int pass, double partialTicks) { + // Do nothing + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + private void checkOcclusionQueryResult(int x, int z) { + // Do nothing + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void markRenderersForNewPosition(int p_72722_1_, int p_72722_2_, int p_72722_3_) { + // Do nothing + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public boolean updateRenderers(EntityLivingBase e, boolean b){ + AngelicaRenderQueue.processTasks(1); + return true; + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public int sortAndRender(EntityLivingBase entity, int pass, double partialTicks) { + final WorldRenderingPipeline pipeline; + if(!AngelicaConfig.enableIris) { + pipeline = null; + } else { + pipeline = Iris.getPipelineManager().getPipelineNullable(); + pipeline.setPhase(WorldRenderingPhase.TERRAIN_CUTOUT); + + if(pass == 1) { + final Camera camera = new Camera(mc.renderViewEntity, (float) partialTicks); + + // iris$beginTranslucents + pipeline.beginHand(); + HandRenderer.INSTANCE.renderSolid(null /*poseStack*/, (float) partialTicks, camera, mc.renderGlobal, pipeline); + mc.mcProfiler.endStartSection("iris_pre_translucent"); + pipeline.beginTranslucents(); + } + } + // Handle view distance change + if(this.renderDistanceChunks != this.mc.gameSettings.renderDistanceChunks) { + this.loadRenderers(); + } + + RenderHelper.disableStandardItemLighting(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_FOG); + this.mc.entityRenderer.enableLightmap(partialTicks); + // Roughly equivalent to `renderLayer` + RenderDevice.enterManagedCode(); + + final double x = lerp(entity.lastTickPosX, entity.posX, partialTicks); + final double y = lerp(entity.lastTickPosY, entity.posY, partialTicks); + final double z = lerp(entity.lastTickPosZ, entity.posZ, partialTicks); + + try { + final MatrixStack matrixStack = new MatrixStack(ShadowRenderer.ACTIVE ? ShadowRenderer.MODELVIEW : RenderingState.INSTANCE.getModelViewMatrix()); + this.renderer.drawChunkLayer(BlockRenderPass.VALUES[pass], matrixStack, x, y, z); + } finally { + RenderDevice.exitManagedCode(); + this.mc.entityRenderer.disableLightmap(partialTicks); + } + + if(pipeline != null) pipeline.setPhase(WorldRenderingPhase.NONE); + + return 0; + } + + private boolean isSpectatorMode() { + final PlayerControllerMP controller = Minecraft.getMinecraft().playerController; + if(controller == null) + return false; + return controller.currentGameType.getID() == 3; + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void clipRenderersByFrustum(ICamera frustrum, float partialTicks) { + // Roughly equivalent to setupTerrain + RenderDevice.enterManagedCode(); + + final Frustrum frustum = (Frustrum) frustrum; + boolean hasForcedFrustum = false; + boolean spectator = isSpectatorMode(); + Camera camera = new Camera(mc.renderViewEntity, partialTicks); + + try { + this.renderer.updateChunks(camera, frustum, hasForcedFrustum, sodium$frame++, spectator); + } finally { + RenderDevice.exitManagedCode(); + } + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void markBlocksForUpdate(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + // scheduleBlockRenders + this.renderer.scheduleRebuildForBlockArea(minX, minY, minZ, maxX, maxY, maxZ, false); + } + + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void markBlockForUpdate(int x, int y, int z) { + this.renderer.scheduleRebuildForBlockArea(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1, false); + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void markBlockForRenderUpdate(int x, int y, int z) { + // scheduleBlockRenders + this.renderer.scheduleRebuildForBlockArea(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1, false); + } + + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void markBlockRangeForRenderUpdate(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + // scheduleBlockRenders + this.renderer.scheduleRebuildForBlockArea(minX, minY, minZ, maxX, maxY, maxZ, false); + } + + @Inject(method = "loadRenderers", at = @At("RETURN")) + private void onReload(CallbackInfo ci) { + RenderDevice.enterManagedCode(); + + try { + this.renderer.reload(); + } finally { + RenderDevice.exitManagedCode(); + } + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void loadRenderers() { + if (this.theWorld == null) return; + Blocks.leaves.setGraphicsLevel(this.mc.gameSettings.fancyGraphics); + Blocks.leaves2.setGraphicsLevel(this.mc.gameSettings.fancyGraphics); + this.renderDistanceChunks = this.mc.gameSettings.renderDistanceChunks; + this.worldRenderers = null; + this.sortedWorldRenderers = null; + + this.renderChunksWide = 0; + this.renderChunksTall = 0; + this.renderChunksDeep = 0; + } + + @Inject(method="renderEntities", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/RenderHelper;enableStandardItemLighting()V", shift = At.Shift.AFTER)) + public void sodium$renderTileEntities(EntityLivingBase entity, ICamera camera, float partialTicks, CallbackInfo ci) { + this.renderer.renderTileEntities(entity, camera, partialTicks); + } + + @Redirect(method="renderEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isInRangeToRender3d(DDD)Z")) + private boolean isInRange(Entity e, double x, double y, double z) { + // TODO: this check is done slightly earlier than Sodium does it, make sure it doesn't cull too much + return e.isInRangeToRender3d(x, y, z) && SodiumWorldRenderer.getInstance().isEntityVisible(e); + } + + @Redirect(method = "renderClouds", at = @At(value = "FIELD", target = "Lnet/minecraft/client/settings/GameSettings;fancyGraphics:Z", opcode = Opcodes.GETFIELD, ordinal = 0)) + private boolean redirectGetFancyClouds(GameSettings settings) { + return SodiumClientMod.options().quality.cloudQuality.isFancy(); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderManager.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderManager.java new file mode 100644 index 000000000..5484c81a8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderManager.java @@ -0,0 +1,26 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.gui.options.named.LightingQuality; +import me.jellysquid.mods.sodium.client.model.light.EntityLighter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(RenderManager.class) +public class MixinRenderManager { + + /** + * Sodium: Use Sodium smooth entity light if enabled. + */ + @Redirect(method = "renderEntityStatic", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getBrightnessForRender(F)I")) + private int sodium$getBrightnessForRender(Entity self, float partialTicks) { + if (Minecraft.getMinecraft().gameSettings.ambientOcclusion == LightingQuality.HIGH.getVanilla()) { + return EntityLighter.getBlendedLight(self, partialTicks); + } + + return self.getBrightnessForRender(partialTicks); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTessellator.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTessellator.java new file mode 100644 index 000000000..b4507c8d7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTessellator.java @@ -0,0 +1,90 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import com.gtnewhorizons.angelica.glsm.TessellatorManager; +import com.gtnewhorizons.angelica.mixins.interfaces.ITessellatorInstance; +import net.minecraft.client.renderer.Tessellator; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.nio.Buffer; +import java.nio.ByteBuffer; + +@Mixin(Tessellator.class) +public abstract class MixinTessellator implements ITessellatorInstance { + @Shadow public int vertexCount; + @Shadow public boolean isDrawing; + public float angelica$normalX; + public float angelica$normalY; + public float angelica$normalZ; + public float angelica$midTextureU; + public float angelica$midTextureV; + + @Shadow public abstract void reset(); + + /** + * @reason Allow using multiple tessellator instances concurrently by removing static field access in alternate instances. + **/ + @Redirect(method = "reset", at = @At(value = "INVOKE", target = "Ljava/nio/ByteBuffer;clear()Ljava/nio/Buffer;")) + private Buffer removeStaticBufferResetOutsideSingleton(ByteBuffer buffer) { + if(TessellatorManager.isMainInstance(this)) { + return buffer.clear(); + } + return buffer; + } + + @Inject(method="draw", at=@At("HEAD")) + private void preventOffMainThreadDrawing(CallbackInfoReturnable cir) { + if(!TessellatorManager.isMainInstance(this)) { + throw new RuntimeException("Tried to draw on a tessellator that isn't on the main thread!"); + } + } + + @Inject(method = "setNormal(FFF)V", at = @At("HEAD")) + private void angelica$captureNormalComponents(float x, float y, float z, CallbackInfo ci) { + this.angelica$normalX = x; + this.angelica$normalY = y; + this.angelica$normalZ = z; + } + + + // New methods from ITesselatorInstance + + @Override + public float getNormalX() { + return angelica$normalX; + } + + @Override + public float getNormalY() { + return angelica$normalY; + } + + @Override + public float getNormalZ() { + return angelica$normalZ; + } + + @Override + public void discard() { + isDrawing = false; + reset(); + } + + @Override + public float getMidTextureU() { + return angelica$midTextureU; + } + + @Override + public float getMidTextureV() { + return angelica$midTextureV; + } + + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureAtlasSprite.java new file mode 100644 index 000000000..a6e6550ec --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureAtlasSprite.java @@ -0,0 +1,79 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.render.texture.SpriteExtended; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureUtil; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; + +@Mixin(TextureAtlasSprite.class) +public class MixinTextureAtlasSprite implements SpriteExtended { + private boolean forceNextUpdate; + + @Shadow + protected List framesTextureData; + + @Shadow + private AnimationMetadataSection animationMetadata; + + @Shadow + protected int originX; + + @Shadow + protected int originY; + + @Shadow + protected int width; + + @Shadow + protected int height; + + @Shadow + protected int frameCounter; + + @Shadow + protected int tickCounter; + + /** + * @author JellySquid & Asek3 + * @reason To optimal solution it's better to overwrite + */ + @Overwrite + public void updateAnimation() { + ++this.tickCounter; + + boolean onDemand = SodiumClientMod.options().advanced.animateOnlyVisibleTextures; + + if (!onDemand || this.forceNextUpdate) { + this.uploadTexture(); + } + } + + private void uploadTexture() { + if (this.tickCounter >= this.animationMetadata.getFrameTimeSingle(this.frameCounter)) { + int prevFrameIndex = this.animationMetadata.getFrameIndex(this.frameCounter); + int frameCount = this.animationMetadata.getFrameCount() == 0 ? this.framesTextureData.size() : this.animationMetadata.getFrameCount(); + + this.frameCounter = (this.frameCounter + 1) % frameCount; + this.tickCounter = 0; + + int frameIndex = this.animationMetadata.getFrameIndex(this.frameCounter); + + if (prevFrameIndex != frameIndex && frameIndex >= 0 && frameIndex < this.framesTextureData.size()) { + TextureUtil.uploadTextureMipmap(this.framesTextureData.get(frameIndex), this.width, this.height, this.originX, this.originY, false, false); + } + } + + this.forceNextUpdate = false; + } + + @Override + public void markActive() { + this.forceNextUpdate = true; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureMap.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureMap.java new file mode 100644 index 000000000..f23aa5606 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTextureMap.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.render.texture.SpriteUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TextureMap.class) +public class MixinTextureMap { + + @Inject(method = "getAtlasSprite", at = @At("RETURN")) + private void preReturnSprite(CallbackInfoReturnable cir) { + TextureAtlasSprite sprite = cir.getReturnValue(); + + if (sprite != null) { + SpriteUtil.markSpriteActive(sprite); + } + } + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldClient.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldClient.java new file mode 100644 index 000000000..2fce47c04 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldClient.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import net.minecraft.client.multiplayer.WorldClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WorldClient.class) +public class MixinWorldClient { + @Inject(method = "doPreChunk", at = @At("TAIL")) + private void sodium$loadChunk(int x, int z, boolean load, CallbackInfo ci) { + if(load) + SodiumWorldRenderer.getInstance().onChunkAdded(x, z); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldRenderer.java new file mode 100644 index 000000000..fa5a18453 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinWorldRenderer.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium; + +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.entity.EntityLivingBase; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +// Let other mixins apply, and then overwrite them +@Mixin(value=WorldRenderer.class, priority = 2000) +public class MixinWorldRenderer { + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void updateRenderer(EntityLivingBase e){ + // Do nothing + } + + /** + * @author Sodium + * @reason Redirect to our renderer + */ + @Overwrite + public void updateRendererSort(EntityLivingBase e){ + // Do nothing + } + + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/startup/MixinInitDebug.java b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/startup/MixinInitDebug.java new file mode 100644 index 000000000..253c90b56 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/early/sodium/startup/MixinInitDebug.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.mixins.early.sodium.startup; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.Iris; +import net.minecraft.client.renderer.OpenGlHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(OpenGlHelper.class) +public class MixinInitDebug { + @Inject(method = "initializeTextures", at = @At("RETURN")) + private static void sodium$initIrisDebug(CallbackInfo ci) { + if (Thread.currentThread() != GLStateManager.getMainThread()) { + AngelicaTweaker.LOGGER.warn("Renderer initialization called from non-main thread!"); + return; + } + // Temp -- move this into common debug code + Iris.identifyCapabilities(); + Iris.setDebug(true); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ExtendedNibbleArray.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ExtendedNibbleArray.java new file mode 100644 index 000000000..591b43633 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ExtendedNibbleArray.java @@ -0,0 +1,7 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +public interface ExtendedNibbleArray { + public byte[] getData(); + public int getDepthBits(); + public int getDepthBitsPlusFour(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/FontRendererAccessor.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/FontRendererAccessor.java new file mode 100644 index 000000000..1a60aae6b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/FontRendererAccessor.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer; +import net.minecraft.util.ResourceLocation; + +public interface FontRendererAccessor { + BatchingFontRenderer angelica$getBatcher(); + + void angelica$bindTexture(ResourceLocation location); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java new file mode 100644 index 000000000..43dc253eb --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +public interface IPatchedTextureAtlasSprite { + + void markNeedsAnimationUpdate(); + + boolean needsAnimationUpdate(); + + void unmarkNeedsAnimationUpdate(); + + void updateAnimationsDryRun(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IRenderGlobalExt.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IRenderGlobalExt.java new file mode 100644 index 000000000..fc262db3d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IRenderGlobalExt.java @@ -0,0 +1,7 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +public interface IRenderGlobalExt { + void scheduleTerrainUpdate(); + int getChunksSubmitted(); + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ISpriteExt.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ISpriteExt.java new file mode 100644 index 000000000..72a8ab671 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ISpriteExt.java @@ -0,0 +1,14 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +import net.minecraft.client.resources.data.AnimationMetadataSection; + +public interface ISpriteExt { + + boolean isAnimation(); + + int getFrame(); + + void callUpload(int frameIndex); + + AnimationMetadataSection getMetadata(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITessellatorInstance.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITessellatorInstance.java new file mode 100644 index 000000000..983870a3c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITessellatorInstance.java @@ -0,0 +1,13 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +public interface ITessellatorInstance { + float getNormalX(); + float getNormalY(); + float getNormalZ(); + + float getMidTextureU(); + + float getMidTextureV(); + + void discard(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITexturesCache.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITexturesCache.java new file mode 100644 index 000000000..0cbfe99fd --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/ITexturesCache.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.mixins.interfaces; + +import net.minecraft.util.IIcon; + +import java.util.Set; + +public interface ITexturesCache { + + Set getRenderedTextures(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/client/journeymap/MixinTileDrawStep.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/client/journeymap/MixinTileDrawStep.java new file mode 100644 index 000000000..6bd156dbd --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/client/journeymap/MixinTileDrawStep.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.angelica.mixins.late.client.journeymap; + +import journeymap.client.render.map.TileDrawStep; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(TileDrawStep.class) +public class MixinTileDrawStep { + @Redirect(method = "*", at = @At(value = "FIELD", opcode = Opcodes.GETFIELD, target = "Ljourneymap/client/render/map/TileDrawStep;debug:Z", remap = false)) + private boolean getDebug(TileDrawStep instance) { + return false; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinModelRotationRenderer.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinModelRotationRenderer.java deleted file mode 100644 index 77053b032..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinModelRotationRenderer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.late.compat; - -import net.minecraft.client.model.ModelRenderer; -import net.smart.render.ModelRotationRenderer; - -import org.spongepowered.asm.mixin.Mixin; - -import com.gtnewhorizons.angelica.mixins.early.renderer.MixinModelRenderer; - -@Mixin(ModelRotationRenderer.class) -public class MixinModelRotationRenderer extends MixinModelRenderer { - - @Override - public void angelica$resetDisplayList() { - super.angelica$resetDisplayList(); - ((ModelRenderer) (Object) this).compiled = false; - ((ModelRenderer) (Object) this).displayList = 0; - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinRenderHalo.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinRenderHalo.java deleted file mode 100644 index 2836b6aa2..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/late/compat/MixinRenderHalo.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.late.compat; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import com.gtnewhorizons.angelica.client.Shaders; - -import mrtjp.projectred.core.RenderHalo; - -@Mixin(value = RenderHalo.class) -public class MixinRenderHalo { - - @Inject( - at = @At(remap = false, target = "Lcodechicken/lib/render/CCRenderState;reset()V", value = "INVOKE"), - method = "prepareRenderState()V", - remap = false) - private void angelica$beginProjectRedHalo(CallbackInfo ci) { - Shaders.beginProjectRedHalo(); - } - - @Inject(at = @At("TAIL"), method = "restoreRenderState()V", remap = false) - private void angelica$endProjectRedHalo(CallbackInfo ci) { - Shaders.endProjectRedHalo(); - } - -} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/thaumcraft/MixinBlockMagicalLeaves.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/thaumcraft/MixinBlockMagicalLeaves.java new file mode 100644 index 000000000..51226d65f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/thaumcraft/MixinBlockMagicalLeaves.java @@ -0,0 +1,54 @@ +package com.gtnewhorizons.angelica.mixins.late.notfine.leaves.thaumcraft; + +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.util.LeafRenderUtil; +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import thaumcraft.common.blocks.BlockMagicalLeaves; + +@Mixin(value = BlockMagicalLeaves.class) +public abstract class MixinBlockMagicalLeaves extends Block implements ILeafBlock { + + @Override + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + int maskedMeta = world.getBlockMetadata(x, y, z) & 3; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + maskedMeta = maskedMeta > 1 ? 0 : maskedMeta; + return icon[renderMode + maskedMeta * 2]; + } + + /** + * @author jss2a98aj + * @reason Support new leaf rendering modes on Thaumcraft leaves. + */ + @Overwrite + public boolean shouldSideBeRendered(IBlockAccess world, int x, int y, int z, int side) { + return LeafRenderUtil.shouldSideBeRendered(world, x, y, z, side); + } + + private MixinBlockMagicalLeaves(Material material) { + super(material); + } + + @Shadow(remap = false) + public IIcon[] icon; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves.java new file mode 100644 index 000000000..a9a51afe4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.mixins.late.notfine.leaves.twilightforest; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import net.minecraft.block.BlockLeaves; +import net.minecraft.init.Blocks; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import twilightforest.block.BlockTFLeaves; + +@Mixin(value = BlockTFLeaves.class) +public abstract class MixinBlockTFLeaves extends BlockLeaves { + + @Override + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + int maskedMeta = world.getBlockMetadata(x, y, z) & 3; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + maskedMeta = maskedMeta > 1 ? 0 : maskedMeta; + return Blocks.leaves.field_150129_M[renderMode][maskedMeta]; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves3.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves3.java new file mode 100644 index 000000000..cab3d6c45 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFLeaves3.java @@ -0,0 +1,33 @@ +package com.gtnewhorizons.angelica.mixins.late.notfine.leaves.twilightforest; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import net.minecraft.block.BlockLeaves; +import net.minecraft.init.Blocks; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import twilightforest.block.BlockTFLeaves3; + +@Mixin(value = BlockTFLeaves3.class) +public abstract class MixinBlockTFLeaves3 extends BlockLeaves { + + @Override + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + return Blocks.leaves.field_150129_M[renderMode][0]; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFMagicLeaves.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFMagicLeaves.java new file mode 100644 index 000000000..a1eea3b40 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/twilightforest/MixinBlockTFMagicLeaves.java @@ -0,0 +1,51 @@ +package com.gtnewhorizons.angelica.mixins.late.notfine.leaves.twilightforest; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import net.minecraft.block.BlockLeaves; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import twilightforest.block.BlockTFMagicLeaves; + +@Mixin(value = BlockTFMagicLeaves.class) +public abstract class MixinBlockTFMagicLeaves extends BlockLeaves { + @Override + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + int maskedMeta = world.getBlockMetadata(x, y, z) & 3; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + maskedMeta = maskedMeta > 1 ? 0 : maskedMeta; + return switch (maskedMeta) { + case 1 -> renderMode == 1 ? SPR_TRANSLEAVES_OPAQUE : SPR_TRANSLEAVES; + case 3 -> renderMode == 1 ? SPR_SORTLEAVES_OPAQUE : SPR_SORTLEAVES; + default -> renderMode == 1 ? SPR_TIMELEAVES_OPAQUE : SPR_TIMELEAVES; + }; + } + + @Shadow(remap = false) + public static IIcon SPR_TIMELEAVES; + @Shadow(remap = false) + public static IIcon SPR_TIMELEAVES_OPAQUE; + @Shadow(remap = false) + public static IIcon SPR_TRANSLEAVES; + @Shadow(remap = false) + public static IIcon SPR_TRANSLEAVES_OPAQUE; + @Shadow(remap = false) + public static IIcon SPR_SORTLEAVES; + @Shadow(remap = false) + public static IIcon SPR_SORTLEAVES_OPAQUE; + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/witchery/MixinBlockWitchLeaves.java b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/witchery/MixinBlockWitchLeaves.java new file mode 100644 index 000000000..1b7e172c5 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/late/notfine/leaves/witchery/MixinBlockWitchLeaves.java @@ -0,0 +1,53 @@ +package com.gtnewhorizons.angelica.mixins.late.notfine.leaves.witchery; + +import com.emoniph.witchery.blocks.BlockWitchLeaves; +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.util.ILeafBlock; +import net.minecraft.block.BlockLeavesBase; +import net.minecraft.block.material.Material; +import net.minecraft.util.Facing; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = BlockWitchLeaves.class) +public abstract class MixinBlockWitchLeaves extends BlockLeavesBase { + + /** + * @author jss2a98aj + * @reason Support new leaf rendering modes on Witchery leaves. + */ + @Overwrite + public boolean isOpaqueCube() { + return SettingsManager.leavesOpaque; + } + + @Override + public IIcon getIcon(IBlockAccess world, int x, int y, int z, int side) { + int renderMode = ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()).ordinal() - 1; + int maskedMeta = world.getBlockMetadata(x, y, z) & 3; + renderMode = switch (renderMode) { + case -1 -> SettingsManager.leavesOpaque ? 1 : 0; + case 4 -> world.getBlock( + x + Facing.offsetsXForSide[side], + y + Facing.offsetsYForSide[side], + z + Facing.offsetsZForSide[side] + ) instanceof ILeafBlock ? 1 : 0; + default -> renderMode > 1 ? 0 : renderMode; + }; + maskedMeta = maskedMeta > 1 ? 0 : maskedMeta; + return iconsForModes[renderMode][maskedMeta]; + } + + @Shadow(remap = false) + private IIcon[][] iconsForModes; + + private MixinBlockWitchLeaves(Material material, boolean unused) { + super(material, unused); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java b/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java new file mode 100644 index 000000000..7e2b7477a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java @@ -0,0 +1,178 @@ +package com.gtnewhorizons.angelica.proxy; + +import com.google.common.base.Objects; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.hudcaching.HUDCaching; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.EventPriority; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; +import me.jellysquid.mods.sodium.client.SodiumDebugScreenHandler; +import net.coderbot.iris.Iris; +import net.coderbot.iris.client.IrisDebugScreenHandler; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.Direction; +import net.minecraft.util.MathHelper; +import net.minecraft.util.MovingObjectPosition; +import net.minecraftforge.client.event.EntityViewRenderEvent; +import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.common.MinecraftForge; + +import java.lang.management.ManagementFactory; +import java.util.Locale; + +public class ClientProxy extends CommonProxy { + + @Override + public void preInit(FMLPreInitializationEvent event) { + FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + } + + @Override + public void init(FMLInitializationEvent event) { + if(AngelicaConfig.enableHudCaching) { + FMLCommonHandler.instance().bus().register(HUDCaching.INSTANCE); + MinecraftForge.EVENT_BUS.register(HUDCaching.INSTANCE); // TODO remove debug stuff, unused registration} + } + if(AngelicaConfig.enableSodium) { + MinecraftForge.EVENT_BUS.register(SodiumDebugScreenHandler.INSTANCE); + } + if(AngelicaConfig.enableIris) { + MinecraftForge.EVENT_BUS.register(IrisDebugScreenHandler.INSTANCE); + + Iris.INSTANCE.fmlInitEvent(); + FMLCommonHandler.instance().bus().register(Iris.INSTANCE); + MinecraftForge.EVENT_BUS.register(Iris.INSTANCE); + + } + } + + @Override + public void postInit(FMLPostInitializationEvent event) { + // Nothing to do here (yet) + } + + float lastIntegratedTickTime; + @SubscribeEvent + public void onTick(TickEvent.ServerTickEvent event) { + if(FMLCommonHandler.instance().getSide().isClient() && event.phase == TickEvent.Phase.END) { + IntegratedServer srv = Minecraft.getMinecraft().getIntegratedServer(); + if(srv != null) { + long currentTickTime = srv.tickTimeArray[srv.getTickCounter() % 100]; + lastIntegratedTickTime = lastIntegratedTickTime * 0.8F + (float)currentTickTime / 1000000.0F * 0.2F; + } else + lastIntegratedTickTime = 0; + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onRenderOverlay(RenderGameOverlayEvent.Text event) { + Minecraft mc = Minecraft.getMinecraft(); + if(event.isCanceled() || !mc.gameSettings.showDebugInfo || event.left.size() < 1) + return; + NetHandlerPlayClient cl = mc.getNetHandler(); + if(cl != null) { + IntegratedServer srv = mc.getIntegratedServer(); + + if (srv != null) { + String s = String.format("Integrated server @ %.0f ms ticks", lastIntegratedTickTime); + event.left.add(1, s); + } + } + if(AngelicaConfig.showBlockDebugInfo && mc.objectMouseOver != null && mc.objectMouseOver.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if(!event.right.isEmpty() && Objects.firstNonNull(event.right.get(event.right.size() - 1), "").length() > 0) + event.right.add(""); + Block block = mc.theWorld.getBlock(mc.objectMouseOver.blockX, mc.objectMouseOver.blockY, mc.objectMouseOver.blockZ); + int meta = mc.theWorld.getBlockMetadata(mc.objectMouseOver.blockX, mc.objectMouseOver.blockY, mc.objectMouseOver.blockZ); + event.right.add(Block.blockRegistry.getNameForObject(block)); + event.right.add("meta: " + meta); + } + if(AngelicaConfig.modernizeF3Screen) { + boolean hasReplacedXYZ = false; + for(int i = 0; i < event.left.size() - 3; i++) { + /* These checks should not be inefficient as most of the time the first one will already fail */ + if(!hasReplacedXYZ && Objects.firstNonNull(event.left.get(i), "").startsWith("x:") + && Objects.firstNonNull(event.left.get(i + 1), "").startsWith("y:") + && Objects.firstNonNull(event.left.get(i + 2), "").startsWith("z:") + && Objects.firstNonNull(event.left.get(i + 3), "").startsWith("f:")) { + hasReplacedXYZ = true; + int heading = MathHelper.floor_double((double)(mc.thePlayer.rotationYaw * 4.0F / 360.0F) + 0.5D) & 3; + String heading_str = switch (heading) { + case 0 -> "Towards positive Z"; + case 1 -> "Towards negative X"; + case 2 -> "Towards negative Z"; + case 3 -> "Towards positive X"; + default -> throw new RuntimeException(); + }; + event.left.set(i, String.format("XYZ: %.3f / %.5f / %.3f", mc.thePlayer.posX, mc.thePlayer.boundingBox.minY, mc.thePlayer.posZ)); + int blockX = MathHelper.floor_double(mc.thePlayer.posX); + int blockY = MathHelper.floor_double(mc.thePlayer.boundingBox.minY); + int blockZ = MathHelper.floor_double(mc.thePlayer.posZ); + event.left.set(i + 1, String.format("Block: %d %d %d [%d %d %d]", blockX, blockY, blockZ, blockX & 15, blockY & 15, blockZ & 15)); + event.left.set(i + 2, String.format("Chunk: %d %d %d", blockX >> 4, blockY >> 4, blockZ >> 4)); + event.left.set(i + 3, String.format("Facing: %s (%s) (%.1f / %.1f)", Direction.directions[heading].toLowerCase(Locale.ROOT), heading_str, MathHelper.wrapAngleTo180_float(mc.thePlayer.rotationYaw), MathHelper.wrapAngleTo180_float(mc.thePlayer.rotationPitch))); + } + } + event.setCanceled(true); + /* render ourselves for modern background */ + FontRenderer fontrenderer = mc.fontRenderer; + int fontColor = 0xe0e0e0; + int rectColor = 0x90505050; + for (int x = 0; x < event.left.size(); x++) + { + String msg = event.left.get(x); + if (msg == null) continue; + int strX = 2; + int strY = 2 + x * fontrenderer.FONT_HEIGHT; + Gui.drawRect(1, strY - 1, strX + fontrenderer.getStringWidth(msg) + 1, strY + fontrenderer.FONT_HEIGHT - 1, rectColor); + fontrenderer.drawString(msg, strX, strY, fontColor); + } + int width = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight).getScaledWidth(); + for (int x = 0; x < event.right.size(); x++) + { + String msg = event.right.get(x); + if (msg == null) continue; + int w = fontrenderer.getStringWidth(msg); + int strX = width - w - 2; + int strY = 2 + x * fontrenderer.FONT_HEIGHT; + Gui.drawRect(strX - 1, strY - 1, strX + w + 1, strY + fontrenderer.FONT_HEIGHT - 1, rectColor); + fontrenderer.drawString(msg, strX, strY, fontColor); + } + } + } + + private float gameStartTime = -1; + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onGuiOpen(GuiOpenEvent event) { + if(!event.isCanceled() && event.gui instanceof GuiMainMenu && gameStartTime == -1) { + gameStartTime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000f; + AngelicaTweaker.LOGGER.info("The game loaded in " + gameStartTime + " seconds."); + } + } + + /* coerce NaN fog values back to 0 (https://bugs.mojang.com/browse/MC-10480) - from ArchaicFix */ + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onFogColor(EntityViewRenderEvent.FogColors event) { + if(Float.isNaN(event.red)) + event.red = 0f; + if(Float.isNaN(event.green)) + event.green = 0f; + if(Float.isNaN(event.blue)) + event.blue = 0f; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java b/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java new file mode 100644 index 000000000..bf06acc3d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java @@ -0,0 +1,16 @@ +package com.gtnewhorizons.angelica.proxy; + +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; + +public class CommonProxy { + + public void preInit(FMLPreInitializationEvent event) { + + } + + public void init(FMLInitializationEvent event) {} + + public void postInit(FMLPostInitializationEvent event) {} +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaBlockSafetyRegistry.java b/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaBlockSafetyRegistry.java new file mode 100644 index 000000000..b857a97b7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaBlockSafetyRegistry.java @@ -0,0 +1,47 @@ +package com.gtnewhorizons.angelica.rendering; + +import it.unimi.dsi.fastutil.objects.Reference2BooleanMap; +import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap; +import net.minecraft.block.Block; + +import java.util.concurrent.locks.StampedLock; + +public class AngelicaBlockSafetyRegistry { + private static final Reference2BooleanMap SAFETY_MAP = new Reference2BooleanOpenHashMap<>(); + private static final StampedLock LOCK = new StampedLock(); + + public static boolean canBlockRenderOffThread(Block block) { + long stamp = LOCK.readLock(); + boolean isOffThread, shouldPopulate; + try { + isOffThread = SAFETY_MAP.getBoolean(block); + if (isOffThread) { + return true; // no need to check if 'false' was due to not being populated + } + + shouldPopulate = !SAFETY_MAP.containsKey(block); + } finally { + LOCK.unlock(stamp); + } + + if(shouldPopulate) { + isOffThread = populateCanRenderOffThread(block); + } + + return isOffThread; + } + + private static boolean populateCanRenderOffThread(Block block) { + boolean canBeOffThread = !(block.getClass().getName().startsWith("gregtech.")); + + long stamp = LOCK.writeLock(); + + try { + SAFETY_MAP.put(block, canBeOffThread); + } finally { + LOCK.unlock(stamp); + } + + return canBeOffThread; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaRenderQueue.java b/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaRenderQueue.java new file mode 100644 index 000000000..fb2058784 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/AngelicaRenderQueue.java @@ -0,0 +1,52 @@ +package com.gtnewhorizons.angelica.rendering; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public class AngelicaRenderQueue { + private static final Thread MAIN_THREAD = Thread.currentThread(); + private static final Queue TASKS = new ConcurrentLinkedQueue<>(); + + private static final Executor EXECUTOR = (runnable) -> { + if(Thread.currentThread() == MAIN_THREAD) { + runnable.run(); + } else { + TASKS.add(runnable); + LockSupport.unpark(MAIN_THREAD); + } + }; + + public static Executor executor() { + return EXECUTOR; + } + + public static int processTasks(int max) { + int tasksRun = 0; + while(tasksRun < max) { + Runnable r = TASKS.poll(); + if(r == null) + break; + r.run(); + tasksRun++; + } + return tasksRun; + } + + public static int processTasks() { + return processTasks(Integer.MAX_VALUE); + } + + private static final long WAIT_TIME = TimeUnit.MILLISECONDS.toNanos(50); + + public static void managedBlock(BooleanSupplier isDone) { + while(!isDone.getAsBoolean()) { + if(AngelicaRenderQueue.processTasks(1) == 0) { + LockSupport.parkNanos(WAIT_TIME); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/RenderingState.java b/src/main/java/com/gtnewhorizons/angelica/rendering/RenderingState.java new file mode 100644 index 000000000..a532442a4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/RenderingState.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.rendering; + +import lombok.Getter; +import org.joml.Matrix4f; +import org.joml.Vector3d; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; + +public class RenderingState { + public static final RenderingState INSTANCE = new RenderingState(); + @Getter + private final Vector3d cameraPosition = new Vector3d(); + @Getter + private final FloatBuffer projectionBuffer = BufferUtils.createFloatBuffer(16); + @Getter + private final FloatBuffer modelViewBuffer = BufferUtils.createFloatBuffer(16); + @Getter + private final Matrix4f projectionMatrix = new Matrix4f().identity(); + @Getter + private final Matrix4f modelViewMatrix = new Matrix4f().identity(); + + + public void setCameraPosition(double x, double y, double z) { + cameraPosition.set(x, y, z); + } + + public void captureProjectionMatrix() { + // Not very fast, but given we're not on 3.2+ core and are still using the opengl matrix stack, we don't have much alternative + projectionBuffer.position(0); + GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projectionBuffer); + projectionMatrix.set(projectionBuffer); + } + + public void captureModelViewMatrix() { + modelViewBuffer.position(0); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, modelViewBuffer); + modelViewMatrix.set(modelViewBuffer); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/ACTEntityRenderer.java b/src/main/java/com/gtnewhorizons/angelica/transform/ACTEntityRenderer.java deleted file mode 100644 index 76566d6fb..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/ACTEntityRenderer.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.launchwrapper.IClassTransformer; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -/** Transformer for {@link EntityRenderer} */ -public class ACTEntityRenderer implements IClassTransformer { - - @Override - public byte[] transform(String name, String transformedName, byte[] basicClass) { - AngelicaTweaker.LOGGER.debug("transforming %s %s", name, transformedName); - ClassReader cr = new ClassReader(basicClass); - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - CVTransform cv = new CVTransform(cw); - cr.accept(cv, 0); - return cw.toByteArray(); - } - - private static class CVTransform extends ClassVisitor { - - private String classname; - - public CVTransform(ClassVisitor cv) { - super(Opcodes.ASM4, cv); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, - String[] interfaces) { - classname = name; - cv.visit(version, access, name, signature, superName, interfaces); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - if (Names.entityRenderer_renderHand.equalsNameDesc(name, desc)) { - AngelicaTweaker.LOGGER.trace(" patching method %s.%s%s", classname, name, desc); - return new MVrenderHand(cv.visitMethod(access, name, desc, signature, exceptions)); - } - return cv.visitMethod(access, name, desc, signature, exceptions); - } - } - - private static class MVrenderHand extends MethodVisitor { - - private Label label; - - public MVrenderHand(MethodVisitor mv) { - super(Opcodes.ASM4, mv); - label = new Label(); - } - - @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { - // Wraps the code from GL11.glPushMatrix() to GL11.glPopMatrix() in an if(!Shaders.isHandRendered) check - if (Names.equals("org/lwjgl/opengl/GL11", "glPushMatrix", "()V", owner, name, desc)) { - mv.visitFieldInsn( - Opcodes.GETSTATIC, - "com/gtnewhorizons/angelica/client/Shaders", - "isHandRendered", - "Z"); - mv.visitJumpInsn(Opcodes.IFNE, label); - mv.visitMethodInsn(opcode, owner, name, desc); - return; - } else if (Names.equals("org/lwjgl/opengl/GL11", "glPopMatrix", "()V", owner, name, desc)) { - mv.visitMethodInsn(opcode, owner, name, desc); - mv.visitLabel(label); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - return; - } - mv.visitMethodInsn(opcode, owner, name, desc); - } - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/ACTRendererLivingEntity.java b/src/main/java/com/gtnewhorizons/angelica/transform/ACTRendererLivingEntity.java deleted file mode 100644 index fa8ad5c1e..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/ACTRendererLivingEntity.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import static org.objectweb.asm.Opcodes.GETSTATIC; -import static org.objectweb.asm.Opcodes.IFNE; -import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; - -import net.minecraft.client.renderer.entity.RendererLivingEntity; -import net.minecraft.launchwrapper.IClassTransformer; - -import org.lwjgl.opengl.GL12; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -/** Transformer for {@link RendererLivingEntity} */ -public class ACTRendererLivingEntity implements IClassTransformer { - - @Override - public byte[] transform(String name, String transformedName, byte[] basicClass) { - AngelicaTweaker.LOGGER.debug("transforming {} {}", name, transformedName); - ClassReader cr = new ClassReader(basicClass); - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - CVTransform cv = new CVTransform(cw); - cr.accept(cv, 0); - return cw.toByteArray(); - } - - private static class CVTransform extends ClassVisitor { - - private String classname; - - private CVTransform(ClassVisitor cv) { - super(Opcodes.ASM4, cv); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, - String[] interfaces) { - classname = name; - cv.visit(version, access, name, signature, superName, interfaces); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - if (Names.rendererLivingE_doRender.equalsNameDesc(name, desc)) { - AngelicaTweaker.LOGGER.trace(" patching method {}.{}{}", classname, name, desc); - return new MVdoRenderLiving(cv.visitMethod(access, name, desc, signature, exceptions)); - } - return cv.visitMethod(access, name, desc, signature, exceptions); - } - } - - /** - * Wraps everything in - * {@link RendererLivingEntity#doRender(net.minecraft.entity.EntityLivingBase, double, double, double, float, float)} - * between {@code this.renderEquippedItems(p_76986_1_, p_76986_9_);} and - * {@code GL11.glDisable(GL12.GL_RESCALE_NORMAL);} in an {@code if (!Shaders.useEntityHurtFlash)}-block - */ - private static class MVdoRenderLiving extends MethodVisitor { - - private MVdoRenderLiving(MethodVisitor mv) { - super(Opcodes.ASM4, mv); - } - - /** end of vanilla hurt rendering */ - private Label labelEndVH = null; - - @Override - public void visitLdcInsn(Object cst) { - if (cst instanceof Integer integer) { - if (integer.intValue() == GL12.GL_RESCALE_NORMAL) { - if (labelEndVH != null) { - mv.visitLabel(labelEndVH); - labelEndVH = null; - } - } - } - mv.visitLdcInsn(cst); - } - - @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { - if (opcode == INVOKEVIRTUAL && Names.rendererLivingE_renderEquippedItems.equals(owner, name, desc)) { - mv.visitMethodInsn(opcode, owner, name, desc); - mv.visitFieldInsn(GETSTATIC, "com/gtnewhorizons/angelica/client/Shaders", "useEntityHurtFlash", "Z"); - labelEndVH = new Label(); - mv.visitJumpInsn(IFNE, labelEndVH); - return; - } - mv.visitMethodInsn(opcode, owner, name, desc); - } - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/AClassTransformer.java b/src/main/java/com/gtnewhorizons/angelica/transform/AClassTransformer.java deleted file mode 100644 index cb70f042d..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/AClassTransformer.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import java.util.HashMap; -import java.util.Map; - -import net.minecraft.launchwrapper.IClassTransformer; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.tree.ClassNode; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -public class AClassTransformer implements IClassTransformer { - - /** map of class transformer */ - protected Map ctMap; - - public void put(Names.Clas clas, IClassTransformer ct) { - ctMap.put(clas.clas.replace('/', '.'), ct); - } - - public AClassTransformer() { - InitNames.init(); - ctMap = new HashMap<>(); - put(Names.entityRenderer_, new ACTEntityRenderer()); - put(Names.rendererLivingE_, new ACTRendererLivingEntity()); - } - - @Override - public byte[] transform(String name, String transformedName, byte[] basicClass) { - byte[] bytecode = basicClass; - IClassTransformer ct = ctMap.get(transformedName); - if (ct != null) { - bytecode = ct.transform(name, transformedName, bytecode); - // HACK: Fix stackframes - ClassNode node = new ClassNode(); - ClassReader reader = new ClassReader(bytecode); - reader.accept(node, ClassReader.SKIP_FRAMES); - ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); - node.accept(writer); - bytecode = writer.toByteArray(); - // END HACK - int oldLength = basicClass.length; - int newLength = bytecode.length; - AngelicaTweaker.LOGGER.debug(" {} (+{})", newLength, newLength - oldLength); - } - return bytecode; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java b/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java new file mode 100644 index 000000000..b74a90512 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java @@ -0,0 +1,123 @@ +/*** + * This Class is derived from the ASM ClassReader + *

+ * ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All + * rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or other materials provided with the + * distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.gtnewhorizons.angelica.transform; + +import org.objectweb.asm.Opcodes; + +import java.nio.charset.StandardCharsets; + +/** + * Using this class to search for a (single) String reference is > 40 times faster than parsing a class with a ClassReader + + * ClassNode while using way less RAM + */ +public class ClassConstantPoolParser { + + private static final int UTF8 = 1; + private static final int INT = 3; + private static final int FLOAT = 4; + private static final int LONG = 5; + private static final int DOUBLE = 6; + private static final int FIELD = 9; + private static final int METH = 10; + private static final int IMETH = 11; + private static final int NAME_TYPE = 12; + private static final int HANDLE = 15; + private static final int INDY = 18; + + private final byte[][] BYTES_TO_SEARCH; + + public ClassConstantPoolParser(String... strings) { + BYTES_TO_SEARCH = new byte[strings.length][]; + for (int i = 0; i < BYTES_TO_SEARCH.length; i++) { + BYTES_TO_SEARCH[i] = strings[i].getBytes(StandardCharsets.UTF_8); + } + } + + /** + * Returns true if the constant pool of the class represented by this byte array contains one of the Strings we are looking + * for + */ + public boolean find(byte[] basicClass) { + if (basicClass == null || basicClass.length == 0) { + return false; + } + // checks the class version + if (readShort(6, basicClass) > Opcodes.V1_8) { + return false; + } + // parses the constant pool + int n = readUnsignedShort(8, basicClass); + int index = 10; + for (int i = 1; i < n; ++i) { + int size; + switch (basicClass[index]) { + case FIELD: + case METH: + case IMETH: + case INT: + case FLOAT: + case NAME_TYPE: + case INDY: + size = 5; + break; + case LONG: + case DOUBLE: + size = 9; + ++i; + break; + case UTF8: + final int strLen = readUnsignedShort(index + 1, basicClass); + size = 3 + strLen; + label: + for (byte[] bytes : BYTES_TO_SEARCH) { + if (strLen == bytes.length) { + for (int j = index + 3; j < index + 3 + strLen; j++) { + if (basicClass[j] != bytes[j - (index + 3)]) { + break label; + } + } + return true; + } + } + break; + case HANDLE: + size = 4; + break; + default: + size = 3; + break; + } + index += size; + } + return false; + } + + private static short readShort(final int index, byte[] basicClass) { + return (short) (((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF)); + } + + private static int readUnsignedShort(final int index, byte[] basicClass) { + return ((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF); + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java b/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java deleted file mode 100644 index 7fa42b22d..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import net.minecraft.launchwrapper.Launch; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; - -public class InitNames { - - public static void init() { - final boolean obfuscated = !(Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); - AngelicaTweaker.LOGGER.info("Environment obfuscated: {}", obfuscated); - if (obfuscated) { - new NamerSrg().setNames(); - } else { - new NamerMcp().setNames(); - } - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java b/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java deleted file mode 100644 index 1cf2735f5..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import java.util.ArrayList; - -import com.gtnewhorizons.angelica.transform.Names.Clas; -import com.gtnewhorizons.angelica.transform.Names.Fiel; -import com.gtnewhorizons.angelica.transform.Names.Meth; - -public class Namer { - - ArrayList ac = new ArrayList<>(); - ArrayList af = new ArrayList<>(); - ArrayList am = new ArrayList<>(); - - Clas c(String name) { - Clas x = new Clas(name); - if (ac != null) ac.add(x); - return x; - } - - Fiel f(Clas clas, String name, String desc) { - Fiel x = new Fiel(clas, name, desc); - if (af != null) af.add(x); - return x; - } - - Fiel f(Clas clas, Fiel fiel) { - Fiel x = new Fiel(clas, fiel.name, fiel.desc); - if (af != null) af.add(x); - return x; - } - - Meth m(Clas clas, String name, String desc) { - Meth x = new Meth(clas, name, desc); - if (am != null) am.add(x); - return x; - } - - Meth m(Clas clas, Meth meth) { - Meth x = new Meth(clas, meth.name, meth.desc); - if (am != null) am.add(x); - return x; - } - - public void setNames() {} -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java b/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java deleted file mode 100644 index 42225d8c5..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import static com.gtnewhorizons.angelica.transform.Names.entityLivingBase_; -import static com.gtnewhorizons.angelica.transform.Names.entityRenderer_; -import static com.gtnewhorizons.angelica.transform.Names.entityRenderer_renderHand; -import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_; -import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_doRender; -import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_renderEquippedItems; - -public class Namer1_7_10 extends Namer { - - public void setNames() { - setNames1_7_10(); - } - - public void setNames1_7_10() { - entityRenderer_ = c("blt"); - rendererLivingE_ = c("boh"); - entityLivingBase_ = c("sv"); - - entityRenderer_renderHand = m(entityRenderer_, "b", "(FI)V"); - rendererLivingE_doRender = m(rendererLivingE_, "a", "(" + entityLivingBase_.desc + "DDDFF)V"); - rendererLivingE_renderEquippedItems = m(rendererLivingE_, "c", "(" + entityLivingBase_.desc + "F)V"); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java deleted file mode 100644 index 1a7f08ca2..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -public class NamerMcf extends NamerMcp { - - public void setNames() { - setNamesSrg(); - rename("../build/unpacked/conf/"); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java deleted file mode 100644 index d60745e26..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import com.gtnewhorizons.angelica.loading.AngelicaTweaker; -import com.gtnewhorizons.angelica.transform.Names.Clas; -import com.gtnewhorizons.angelica.transform.Names.Fiel; -import com.gtnewhorizons.angelica.transform.Names.Meth; - -public class NamerMcp extends NamerSrg { - - public void setNames() { - setNamesSrg(); - final String confPath = System.getProperty("net.minecraftforge.gradle.GradleStart.csvDir", "../conf") + "/"; - lookupReobfName(confPath); - rename(confPath); - } - - public void rename(String confPath) { - Map nameMap; - nameMap = loadNameMapCSV(confPath + "fields.csv"); - for (Fiel f : af) { - String s = nameMap.get(f.name); - if (s != null) { - f.name = s; - } - } - nameMap = loadNameMapCSV(confPath + "methods.csv"); - for (Meth m : am) { - String s = nameMap.get(m.name); - if (s != null) { - m.name = s; - } - } - } - - Map loadNameMapCSV(String fileName) { - Map map = new HashMap<>(); - BufferedReader rd = null; - try { - rd = new BufferedReader(new FileReader(fileName)); - String line; - rd.readLine(); // skip first line; - while ((line = rd.readLine()) != null) { - String[] tokens = line.split(","); - if (tokens.length > 1) { - map.put(tokens[0], tokens[1]); - } - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (rd != null) { - try { - rd.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return map; - } - - void lookupReobfName(String confPath) { - Map nameMap; - nameMap = loadReobfMap(confPath + "packaged.srg"); - for (Clas c : ac) { - String s = nameMap.get(c.name); - AngelicaTweaker.LOGGER.trace("C {} {}", c.name, s); - } - for (Fiel f : af) { - String s = nameMap.get(f.clas + "/" + f.name); - AngelicaTweaker.LOGGER.trace("F {} {}", f.name, s); - } - for (Meth m : am) { - String s = nameMap.get(m.clas + "/" + m.name + m.desc); - AngelicaTweaker.LOGGER.trace("M {} {}", m.name, s); - } - } - - Map loadReobfMap(String fileName) { - Map map = new HashMap<>(); - BufferedReader rd = null; - try { - rd = new BufferedReader(new FileReader(fileName)); - String line; - while ((line = rd.readLine()) != null) { - String[] tokens = line.split(" "); - if (tokens.length > 1) { - if ("CL:".equals(tokens[0])) { - map.put(tokens[2], tokens[1]); - } else if ("FD:".equals(tokens[0])) { - map.put(tokens[2], tokens[1]); - } else if ("MD:".equals(tokens[0])) { - map.put(tokens[3] + tokens[4], tokens[1].substring(tokens[1].lastIndexOf('/') + 1)); - } - } - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (rd != null) { - try { - rd.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return map; - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java deleted file mode 100644 index 95e76b6ee..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -import static com.gtnewhorizons.angelica.transform.Names.*; - -public class NamerSrg extends Namer { - - public void setNames() { - setNamesSrg(); - } - - public void setNamesSrg() { - entityRenderer_ = c("net/minecraft/client/renderer/EntityRenderer"); - rendererLivingE_ = c("net/minecraft/client/renderer/entity/RendererLivingEntity"); - entityLivingBase_ = c("net/minecraft/entity/EntityLivingBase"); - - entityRenderer_renderHand = m(entityRenderer_, "func_78476_b", "(FI)V"); - rendererLivingE_doRender = m(rendererLivingE_, "func_76986_a", "(" + entityLivingBase_.desc + "DDDFF)V"); - rendererLivingE_renderEquippedItems = m(rendererLivingE_, "func_77029_c", "(" + entityLivingBase_.desc + "F)V"); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Names.java b/src/main/java/com/gtnewhorizons/angelica/transform/Names.java deleted file mode 100644 index 16e0cbda3..000000000 --- a/src/main/java/com/gtnewhorizons/angelica/transform/Names.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.gtnewhorizons.angelica.transform; - -public class Names { - - public static class Name { - - String clas; - String name; - String desc; - - public Name(String clas, String name, String desc) { - this.clas = clas; - this.name = name; - this.desc = desc; - } - - public Name set(String clas, String name, String desc) { - this.clas = clas; - this.name = name; - this.desc = desc; - return this; - } - - public boolean equals(String clas, String name, String desc) { - return this.clas.equals(clas) && this.name.equals(name) && this.desc.equals(desc); - } - } - - public static class Type extends Name { - - public Type(String desc) { - super("", "", desc); - } - - public Type(String name, String desc) { - super(name, name, desc); - } - } - - public static class Clas extends Type { - - public Clas(String name) { - super(name, "L" + name + ";"); - } - - public boolean equals(String clas) { - return this.clas.equals(clas); - } - } - - public static class Fiel extends Name { - - public Fiel(Clas clas, String name, String desc) { - super(clas.clas, name, desc); - } - - public boolean equals(String clas, String name) { - return this.clas.equals(clas) && this.name.equals(name); - } - } - - public static class Meth extends Name { - - public Meth(Clas clas, String name, String desc) { - super(clas.clas, name, desc); - } - - public boolean equalsNameDesc(String name, String desc) { - return this.name.equals(name) && this.desc.equals(desc); - } - } - - static Clas entityRenderer_; - static Clas rendererLivingE_; - static Clas entityLivingBase_; - - static Meth entityRenderer_renderHand; - static Meth rendererLivingE_doRender; - static Meth rendererLivingE_renderEquippedItems; - - public static boolean equals(String clas1, String name1, String desc1, String clas2, String name2, String desc2) { - return clas1.equals(clas2) && name1.equals(name2) && desc1.equals(desc2); - } -} diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java b/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java new file mode 100644 index 000000000..124e84239 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java @@ -0,0 +1,325 @@ +package com.gtnewhorizons.angelica.transform; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.IrisLogging; +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.Launch; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.spongepowered.asm.lib.ClassReader; +import org.spongepowered.asm.lib.ClassWriter; +import org.spongepowered.asm.lib.Opcodes; +import org.spongepowered.asm.lib.tree.AbstractInsnNode; +import org.spongepowered.asm.lib.tree.ClassNode; +import org.spongepowered.asm.lib.tree.FieldInsnNode; +import org.spongepowered.asm.lib.tree.InsnList; +import org.spongepowered.asm.lib.tree.InsnNode; +import org.spongepowered.asm.lib.tree.IntInsnNode; +import org.spongepowered.asm.lib.tree.LdcInsnNode; +import org.spongepowered.asm.lib.tree.MethodInsnNode; +import org.spongepowered.asm.lib.tree.MethodNode; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This transformer redirects all Tessellator.instance field accesses to go through our TessellatorManager. + * As well as redirect some GL calls to our custom GLStateManager + */ +public class RedirectorTransformer implements IClassTransformer { + + private static final boolean ASSERT_MAIN_THREAD = Boolean.parseBoolean(System.getProperty("angelica.assertMainThread", "false")); + private static final boolean DUMP_CLASSES = Boolean.parseBoolean(System.getProperty("angelica.dumpClass", "false")); + private static final String Drawable = "org/lwjgl/opengl/Drawable"; + private static final String GLStateManager = "com/gtnewhorizons/angelica/glsm/GLStateManager"; + private static final String GL11 = "org/lwjgl/opengl/GL11"; + private static final String GL13 = "org/lwjgl/opengl/GL13"; + private static final String GL14 = "org/lwjgl/opengl/GL14"; + private static final String OpenGlHelper = "net/minecraft/client/renderer/OpenGlHelper"; + private static final String EXTBlendFunc = "org/lwjgl/opengl/EXTBlendFuncSeparate"; + private static final String ARBMultiTexture = "org/lwjgl/opengl/ARBMultitexture"; + private static final String TessellatorClass = "net/minecraft/client/renderer/Tessellator"; + private static final String BlockClass = "net/minecraft/block/Block"; + private static final String MinecraftClient = "net.minecraft.client"; + private static final String SplashProgress = "cpw.mods.fml.client.SplashProgress"; + private static final String ThreadedBlockData = "com/gtnewhorizons/angelica/glsm/ThreadedBlockData"; + private static final Set ExcludedMinecraftMainThreadChecks = ImmutableSet.of( + "startGame", "func_71384_a", + "initializeTextures", "func_77474_a" + ); + + private static final List> BlockBoundsFields = ImmutableList.of( + Pair.of("minX", "field_149759_B"), + Pair.of("minY", "field_149760_C"), + Pair.of("minZ", "field_149754_D"), + Pair.of("maxX", "field_149755_E"), + Pair.of("maxY", "field_149756_F"), + Pair.of("maxZ", "field_149757_G") + ); + + private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(GL11, GL13, GL14, OpenGlHelper, EXTBlendFunc, ARBMultiTexture, TessellatorClass, BlockClass); + private static final Map> methodRedirects = new HashMap<>(); + private static final Map glCapRedirects = new HashMap<>(); + private static final List TransformerExclusions = Arrays.asList( + "org.lwjgl", + "com.gtnewhorizons.angelica.glsm.", + "com.gtnewhorizons.angelica.transform", + "me.eigenraven.lwjgl3ify" + ); + private static int remaps = 0; + + static { + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_ALPHA_TEST, "AlphaTest"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_BLEND, "Blend"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_DEPTH_TEST, "DepthTest"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_CULL_FACE, "Cull"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_LIGHTING, "Lighting"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_TEXTURE_2D, "Texture"); + glCapRedirects.put(org.lwjgl.opengl.GL11.GL_FOG, "Fog"); + glCapRedirects.put(org.lwjgl.opengl.GL12.GL_RESCALE_NORMAL, "RescaleNormal"); + methodRedirects.put(GL11, RedirectMap.newMap() + .add("glAlphaFunc") + .add("glBindTexture") + .add("glBlendFunc") + .add("glClearColor") + .add("glColor3b") + .add("glColor3d") + .add("glColor3f") + .add("glColor3ub") + .add("glColor4b") + .add("glColor4d") + .add("glColor4f") + .add("glColor4ub") + .add("glColorMask") + .add("glDeleteTextures") + .add("glDepthFunc") + .add("glDepthMask") + .add("glDrawArrays") + .add("glEndList") + .add("glFog") + .add("glFogf") + .add("glFogi") + .add("glNewList") + .add("glPushAttrib") + .add("glPopAttrib") + .add("glShadeModel") + .add("glTexImage2D") + ); + methodRedirects.put(GL13, RedirectMap.newMap().add("glActiveTexture")); + methodRedirects.put(GL14, RedirectMap.newMap().add("glBlendFuncSeparate", "tryBlendFuncSeparate")); + methodRedirects.put(OpenGlHelper, RedirectMap.newMap() + .add("glBlendFunc", "tryBlendFuncSeparate") + .add("func_148821_a", "tryBlendFuncSeparate")); + methodRedirects.put(EXTBlendFunc, RedirectMap.newMap().add("glBlendFuncSeparateEXT", "tryBlendFuncSeparate")); + methodRedirects.put(ARBMultiTexture, RedirectMap.newMap().add("glActiveTextureARB")); + } + + @Override + public byte[] transform(final String className, String transformedName, byte[] basicClass) { + if (basicClass == null) return null; + + // Ignore classes that are excluded from transformation - Doesn't fully work without the + // TransformerExclusions due to some nested classes + for (String exclusion : TransformerExclusions) { + if (className.startsWith(exclusion)) { + return basicClass; + } + } + + if (!cstPoolParser.find(basicClass)) { + return basicClass; + } + + final ClassReader cr = new ClassReader(basicClass); + final ClassNode cn = new ClassNode(); + cr.accept(cn, 0); + + boolean changed = false; + for (MethodNode mn : cn.methods) { + if (transformedName.equals("net.minecraft.client.renderer.OpenGlHelper") && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) { + continue; + } + boolean redirectInMethod = false; + for (AbstractInsnNode node : mn.instructions.toArray()) { + if (node instanceof MethodInsnNode mNode) { + if (mNode.owner.equals(GL11) && (mNode.name.equals("glEnable") || mNode.name.equals("glDisable")) && mNode.desc.equals("(I)V")) { + final AbstractInsnNode prevNode = node.getPrevious(); + String name = null; + if (prevNode instanceof LdcInsnNode ldcNode) { + name = glCapRedirects.get(((Integer) ldcNode.cst)); + } else if (prevNode instanceof IntInsnNode intNode) { + name = glCapRedirects.get(intNode.operand); + } + if (name != null) { + if (mNode.name.equals("glEnable")) { + name = "enable" + name; + } else { + name = "disable" + name; + } + } + if (IrisLogging.ENABLE_SPAM) { + if (name == null) { + AngelicaTweaker.LOGGER.info("Redirecting call in {} from GL11.{}(I)V to GLStateManager.{}(I)V", transformedName, mNode.name, mNode.name); + } else { + AngelicaTweaker.LOGGER.info("Redirecting call in {} from GL11.{}(I)V to GLStateManager.{}()V", transformedName, mNode.name, name); + } + } + mNode.owner = GLStateManager; + if (name != null) { + mNode.name = name; + mNode.desc = "()V"; + mn.instructions.remove(prevNode); + } + changed = true; + redirectInMethod = true; + remaps++; + } else if (mNode.owner.startsWith(Drawable) && mNode.name.equals("makeCurrent")) { + mNode.setOpcode(Opcodes.INVOKESTATIC); + mNode.owner = GLStateManager; + mNode.desc = "(L" + Drawable + ";)V"; + mNode.itf = false; + changed = true; + if (IrisLogging.ENABLE_SPAM) { + AngelicaTweaker.LOGGER.info("Redirecting call in {} to GLStateManager.makeCurrent()", transformedName); + } + } else { + final Map redirects = methodRedirects.get(mNode.owner); + if (redirects != null && redirects.containsKey(mNode.name)) { + if (IrisLogging.ENABLE_SPAM) { + final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1); + AngelicaTweaker.LOGGER.info("Redirecting call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, redirects.get(mNode.name), mNode.desc); + } + mNode.owner = GLStateManager; + mNode.name = redirects.get(mNode.name); + changed = true; + redirectInMethod = true; + remaps++; + } + } + } + else if (node.getOpcode() == Opcodes.GETSTATIC && node instanceof FieldInsnNode fNode) { + if ((fNode.name.equals("field_78398_a") || fNode.name.equals("instance")) && fNode.owner.equals(TessellatorClass)) { + if (IrisLogging.ENABLE_SPAM) { + AngelicaTweaker.LOGGER.info("Redirecting Tessellator.instance field in {} to TessellatorManager.get()", transformedName); + } + mn.instructions.set(node, new MethodInsnNode(Opcodes.INVOKESTATIC, "com/gtnewhorizons/angelica/glsm/TessellatorManager", "get", "()Lnet/minecraft/client/renderer/Tessellator;", false)); + changed = true; + } + } + else if ((node.getOpcode() == Opcodes.GETFIELD || node.getOpcode() == Opcodes.PUTFIELD) && node instanceof FieldInsnNode fNode) { + if(fNode.owner.equals(BlockClass)) { + Pair fieldToRedirect = null; + for(Pair blockPairs : BlockBoundsFields) { + if(fNode.name.equals(blockPairs.getLeft()) || fNode.name.equals(blockPairs.getRight())) { + fieldToRedirect = blockPairs; + break; + } + } + if(fieldToRedirect != null) { + if (IrisLogging.ENABLE_SPAM) { + AngelicaTweaker.LOGGER.info("Redirecting Block.{} in {} to thread-safe wrapper", fNode.name, transformedName); + } + // Perform the redirect + fNode.name = fieldToRedirect.getLeft(); // use unobfuscated name + fNode.owner = ThreadedBlockData; + // Inject getter before the field access, to turn Block -> ThreadedBlockData + MethodInsnNode getter = new MethodInsnNode(Opcodes.INVOKESTATIC, ThreadedBlockData, "get", "(L" + BlockClass + ";)L" + ThreadedBlockData + ";", false); + if(node.getOpcode() == Opcodes.GETFIELD) { + mn.instructions.insertBefore(fNode, getter); + } else if(node.getOpcode() == Opcodes.PUTFIELD) { + // FIXME: this code assumes doubles + // Stack: Block, double + final InsnList beforePut = new InsnList(); + beforePut.add(new InsnNode(Opcodes.DUP2_X1)); + // Stack: double, Block, double + beforePut.add(new InsnNode(Opcodes.POP2)); + // Stack: double, Block + beforePut.add(getter); + // Stack: double, ThreadedBlockData + beforePut.add(new InsnNode(Opcodes.DUP_X2)); + // Stack: ThreadedBlockData, double, ThreadedBlockData + beforePut.add(new InsnNode(Opcodes.POP)); + // Stack: ThreadedBlockData, double + mn.instructions.insertBefore(fNode, beforePut); + } + changed = true; + } + + } + } + } + if (ASSERT_MAIN_THREAD && redirectInMethod && !transformedName.startsWith(SplashProgress) && !(transformedName.startsWith(MinecraftClient) && ExcludedMinecraftMainThreadChecks.contains(mn.name))) { + mn.instructions.insert(new MethodInsnNode(Opcodes.INVOKESTATIC, GLStateManager, "assertMainThread", "()V", false)); + } + } + + if (changed) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + cn.accept(cw); + final byte[] bytes = cw.toByteArray(); + saveTransformedClass(bytes, transformedName); + return bytes; + } + return basicClass; + } + + private File outputDir = null; + + private void saveTransformedClass(final byte[] data, final String transformedName) { + if (!DUMP_CLASSES) { + return; + } + if (outputDir == null) { + outputDir = new File(Launch.minecraftHome, "ASM_REDIRECTOR"); + try { + FileUtils.deleteDirectory(outputDir); + } catch (IOException ignored) {} + if (!outputDir.exists()) { + //noinspection ResultOfMethodCallIgnored + outputDir.mkdirs(); + } + } + final String fileName = transformedName.replace('.', File.separatorChar); + final File classFile = new File(outputDir, fileName + ".class"); + final File outDir = classFile.getParentFile(); + if (!outDir.exists()) { + //noinspection ResultOfMethodCallIgnored + outDir.mkdirs(); + } + if (classFile.exists()) { + //noinspection ResultOfMethodCallIgnored + classFile.delete(); + } + try (final OutputStream output = Files.newOutputStream(classFile.toPath())) { + output.write(data); + } catch (IOException e) { + AngelicaTweaker.LOGGER.error("Could not save transformed class (byte[]) " + transformedName, e); + } + } + + private static class RedirectMap extends HashMap { + public static RedirectMap newMap() { + return new RedirectMap<>(); + } + + public RedirectMap add(K name) { + this.put(name, name); + return this; + } + + public RedirectMap add(K name, K newName) { + this.put(name, newName); + return this; + } + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java new file mode 100644 index 000000000..0dee6b633 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java @@ -0,0 +1,7 @@ +package com.gtnewhorizons.angelica.utils; + +public enum AnimationMode { + NONE, + VISIBLE_ONLY, + ALL +} diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java new file mode 100644 index 000000000..38ed419a7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java @@ -0,0 +1,31 @@ +package com.gtnewhorizons.angelica.utils; + +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import com.gtnewhorizons.angelica.mixins.interfaces.ITexturesCache; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; + +public class AnimationsRenderUtils { + + public static void markBlockTextureForUpdate(IIcon icon) { + markBlockTextureForUpdate(icon, null); + } + + public static void markBlockTextureForUpdate(IIcon icon, IBlockAccess blockAccess) { + final TextureMap textureMap = Minecraft.getMinecraft().getTextureMapBlocks(); + final TextureAtlasSprite textureAtlasSprite = textureMap.getAtlasSprite(icon.getIconName()); + + if (textureAtlasSprite != null && textureAtlasSprite.hasAnimationMetadata()) { + // null if called by anything but chunk render cache update (for example to get blocks rendered as items in + // inventory) + if (blockAccess instanceof ITexturesCache) { + ((ITexturesCache) blockAccess).getRenderedTextures().add(textureAtlasSprite); + } else { + ((IPatchedTextureAtlasSprite) textureAtlasSprite).markNeedsAnimationUpdate(); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java b/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java new file mode 100644 index 000000000..c1bf72c99 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java @@ -0,0 +1,52 @@ +package com.gtnewhorizons.angelica.utils; + +public class ManagedEnum> { + + private final T[] allValues; + private T value; + + public ManagedEnum(T initialValue) { + if (initialValue == null) throw new IllegalArgumentException(); + value = initialValue; + @SuppressWarnings("unchecked") + T[] allValues = (T[]) value.getClass().getEnumConstants(); + this.allValues = allValues; + } + + public boolean is(T value) { + return this.value == value; + } + + public T next() { + value = allValues[(value.ordinal() + 1) % allValues.length]; + return value; + } + + public void set(T value) { + this.value = value; + } + + public void set(int ordinal) { + this.value = allValues[Math.min(Math.max(0, ordinal), allValues.length - 1)]; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ManagedEnum that = (ManagedEnum) o; + + return value == that.value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value.toString(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java b/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java new file mode 100644 index 000000000..c7df94c19 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.utils; + +public class Mipmaps { + + private static final float[] VALS = new float[256]; + + public static float get(int i) { + return VALS[i & 0xFF]; + } + + public static int getColorComponent(int one, int two, int three, int four, int bits) { + final float f = Mipmaps.get(one >> bits); + final float g = Mipmaps.get(two >> bits); + final float h = Mipmaps.get(three >> bits); + final float i = Mipmaps.get(four >> bits); + final float j = (float) Math.pow((f + g + h + i) * 0.25, 0.45454545454545453); + return (int) (j * 255.0); + } + + static { + for (int i = 0; i < VALS.length; ++i) { + VALS[i] = (float) Math.pow((float) i / 255.0F, 2.2); + } + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/Digraph.java b/src/main/java/de/odysseus/ithaka/digraph/Digraph.java new file mode 100644 index 000000000..4c3af75ca --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/Digraph.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.OptionalInt; +import java.util.Set; + +/** + * Directed graph interface. + * + * @param vertex type + */ +public interface Digraph extends EdgeWeights { + /** + * Get an edge. + * + * @param source source vertex + * @param target target vertex + * @return edge weight (0 if there is no edge from source to target) + */ + OptionalInt get(V source, V target); + + /** + * Edge test. + * + * @param source source vertex + * @param target target vertex + * @return true iff this de.odysseus.ithaka.digraph contains an edge from source to target + */ + boolean contains(V source, V target); + + /** + * Vertex test + * + * @return true iff this de.odysseus.ithaka.digraph contains vertex + */ + boolean contains(V vertex); + + /** + * Add vertex. + * + * @return true iff vertex has been added + */ + boolean add(V vertex); + + /** + * Put an edge. + * Vertices are added automatically if they appear in an edge. + * + * @param source source vertex + * @param target target vertex + * @param weight edge weight + * @return edge weight that has been previously set (0 if there was no edge from source + * to target) + */ + OptionalInt put(V source, V target, int weight); + + /** + * Remove an edge. + * + * @param source source vertex + * @param target target vertex + * @return edge weight that has been previously set (0 if there was no edge from source + * to target) + */ + OptionalInt remove(V source, V target); + + /** + * Remove a vertex. + * + * @param vertex vertex + * @return true iff this de.odysseus.ithaka.digraph contained vertex + */ + boolean remove(V vertex); + + /** + * Remove all vertices. + * + * @param vertices vertices + */ + void removeAll(Collection vertices); + + /** + * Iterate over vertices. + * + * @return vertices + */ + Iterable vertices(); + + /** + * Iterate over edge targets for given source vertex. + * + * @param source source vertex + * @return edge targets of edges starting at source + */ + Iterable targets(V source); + + /** + * @return number of vertices in this de.odysseus.ithaka.digraph + */ + int getVertexCount(); + + /** + * @return sum of edge weights + */ + int totalWeight(); + + /** + * @return number of edges starting at vertex + */ + int getOutDegree(V vertex); + + /** + * @return number of edges in this de.odysseus.ithaka.digraph + */ + int getEdgeCount(); + + /** + * @return true iff this de.odysseus.ithaka.digraph is acyclic (i.e. it is a DAG) + */ + boolean isAcyclic(); + + /** + * Get reverse de.odysseus.ithaka.digraph (same vertices, with edges reversed). + * + * @return reverse de.odysseus.ithaka.digraph + */ + Digraph reverse(); + + /** + * Get induced subgraph (with vertices in this de.odysseus.ithaka.digraph and the given vertex set and edges that appear in this de.odysseus.ithaka.digraph over the given vertex set). + * + * @return subgraph + */ + Digraph subgraph(Set vertices); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java new file mode 100644 index 000000000..120992254 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.OptionalInt; +import java.util.Set; + +/** + * Abstract Digraph adapter. + * A de.odysseus.ithaka.digraph adapter delegates to a de.odysseus.ithaka.digraph supplied at construction time. + * + * @param vertex type + */ +public abstract class DigraphAdapter implements Digraph { + private final Digraph delegate; + + public DigraphAdapter(Digraph delegate) { + this.delegate = delegate; + } + + @Override + public boolean add(V vertex) { + return delegate.add(vertex); + } + + @Override + public boolean contains(V source, V target) { + return delegate.contains(source, target); + } + + @Override + public boolean contains(V vertex) { + return delegate.contains(vertex); + } + + @Override + public OptionalInt get(V source, V target) { + return delegate.get(source, target); + } + + @Override + public int getOutDegree(V vertex) { + return delegate.getOutDegree(vertex); + } + + @Override + public int getEdgeCount() { + return delegate.getEdgeCount(); + } + + @Override + public int getVertexCount() { + return delegate.getVertexCount(); + } + + @Override + public int totalWeight() { + return delegate.totalWeight(); + } + + @Override + public Iterable vertices() { + return delegate.vertices(); + } + + @Override + public OptionalInt put(V source, V target, int edge) { + return delegate.put(source, target, edge); + } + + @Override + public OptionalInt remove(V source, V target) { + return delegate.remove(source, target); + } + + @Override + public boolean remove(V vertex) { + return delegate.remove(vertex); + } + + @Override + public void removeAll(Collection vertices) { + delegate.removeAll(vertices); + } + + @Override + public Digraph reverse() { + return delegate.reverse(); + } + + @Override + public Digraph subgraph(Set vertices) { + return delegate.subgraph(vertices); + } + + @Override + public boolean isAcyclic() { + return delegate.isAcyclic(); + } + + @Override + public Iterable targets(V source) { + return delegate.targets(source); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (this.getClass() != obj.getClass()) { + return false; + } + + return delegate.equals(((DigraphAdapter) obj).delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java new file mode 100644 index 000000000..63aa63afc --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +/** + * Digraph factory interface. + * + * @param de.odysseus.ithaka.digraph type + */ +public interface DigraphFactory> { + /** + * Create a de.odysseus.ithaka.digraph. + * + * @return de.odysseus.ithaka.digraph + */ + G create(); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java new file mode 100644 index 000000000..021dfe086 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +/** + * Digraph provider interface. + * + * @param de.odysseus.ithaka.digraph sub-type + * @param de.odysseus.ithaka.digraph type + */ +public interface DigraphProvider> { + /** + * Get a de.odysseus.ithaka.digraph. + * + * @param value value associated with a de.odysseus.ithaka.digraph + * @return de.odysseus.ithaka.digraph + */ + G get(T value); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java b/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java new file mode 100644 index 000000000..973d5a3d4 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java @@ -0,0 +1,352 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; +import java.util.Stack; + +/** + * This class provides some common de.odysseus.ithaka.digraph utilities. + */ +public class Digraphs { + /** + * Get an unmodifiable empty de.odysseus.ithaka.digraph. + * + * @return empty de.odysseus.ithaka.digraph + */ + public static DoubledDigraph emptyDigraph() { + return new EmptyDigraph<>(); + } + + /** + * Wraps the given de.odysseus.ithaka.digraph to make it unmodifiable. Whenever a method + * is called on the resulting de.odysseus.ithaka.digraph that could modify the underlying + * de.odysseus.ithaka.digraph, an exception is thrown. + * + * @param vertex type + * @return unmodifiable de.odysseus.ithaka.digraph equivalent to the given de.odysseus.ithaka.digraph + */ + public static Digraph unmodifiableDigraph(Digraph digraph) { + return new UnmodifiableDigraph<>(digraph); + } + + /** + * Topologically sort vertices of an acyclic directed graph (DAG). + * This method will produce an ordering of vertices, such that all + * edges go from left right. + * If the input graph is not a DAG, the algorithm will still perform, + * but in the resulting list there will be edges from vertices to + * vertices prior in the list. + * + * @param vertex type + * @param digraph input graph + * @param descending let edges go from right to left if true + * @return list of vertices topologically ordered. + */ + public static List toposort(Digraph digraph, boolean descending) { + List finished = new ArrayList<>(); + Set discovered = new HashSet<>(digraph.getVertexCount()); + for (V vertex : digraph.vertices()) { + if (!discovered.contains(vertex)) { + dfs(digraph, vertex, discovered, finished); + } + } + if (!descending) { + Collections.reverse(finished); + } + return finished; + } + + /** + * Compute the set of vertices reachable from the given source in the given de.odysseus.ithaka.digraph. + * + * @param vertex type + * @param source source vertex + * @return the set of vertices reachable from source + */ + public static Set closure(Digraph digraph, V source) { + Set closure = new HashSet<>(); + dfs(digraph, source, closure, closure); + return closure; + } + + /** + * Returns true if this graph is definitely acyclic from some quick checks, + * but false if properly determining whether the graph is acyclic would + * take more work. + */ + public static boolean isTriviallyAcyclic(Digraph digraph) { + return digraph.getVertexCount() < 2; + } + + /** + * Answer true if the given de.odysseus.ithaka.digraph is acyclic (DAG). + * Per definition, the empty graph and single vertex digraphs are acyclic. + * + * @param vertex type + * @return true iff the given de.odysseus.ithaka.digraph is acyclic + */ + public static boolean isAcyclic(Digraph digraph) { + if (isTriviallyAcyclic(digraph)) { + return true; + } + + int n = digraph.getVertexCount(); + + if (digraph.getEdgeCount() > (n * (n - 1)) / 2) { + return false; + } + + return Digraphs.scc(digraph).size() == n; + } + + /** + * Test if the given digraphs are equivalent. + * This is the case if and the same vertices are connected by an edge. + *

    + *
  1. both digraphs contain the same vertices
  2. + *
  3. the same pairs of vertices are connected by an edge in both digraphs
  4. + *
  5. optionally, this method may require that the corresponding edges are equal.
  6. + *
+ * + * @param vertex type + * @param first first de.odysseus.ithaka.digraph. + * @param second second de.odysseus.ithaka.digraph. + * @param compareEdges if true, compare edges using equals(). + * @return true iff the two digraphs are equivalent according to the above description. + */ + public static boolean isEquivalent(Digraph first, Digraph second, boolean compareEdges) { + if (first == second) { + return true; + } + if (first.getEdgeCount() != second.getEdgeCount() || first.getVertexCount() != second.getVertexCount()) { + return false; + } + for (V source : first.vertices()) { + if (!second.contains(source)) { + return false; + } + for (V target : first.targets(source)) { + OptionalInt secondEdge = second.get(source, target); + + if (!secondEdge.isPresent()) { + return false; + } + + if (compareEdges) { + int edge1 = first.get(source, target).getAsInt(); + int edge2 = secondEdge.getAsInt(); + + if (edge1 != edge2) { + return false; + } + } + } + } + return true; + } + + /** + * Answer true if the given de.odysseus.ithaka.digraph is strongly connected. + * Per definition, the empty graph and single vertex digraphs are strongly connected. + * + * @param vertex type + * @return true iff the given de.odysseus.ithaka.digraph is strongly connected + */ + public static boolean isStronglyConnected(Digraph digraph) { + int n = digraph.getVertexCount(); + if (n < 2) { + return true; + } + return Digraphs.scc(digraph).size() == 1; + } + + /** + * Answer true if there is a path from the given source to the given target + * in the supplied graph. If source is equal to target, answer true. + * + * @param vertex type + * @param source source vertex + * @param target target vertex + * @return true iff there's a path from source to target in de.odysseus.ithaka.digraph + */ + public static boolean isReachable(Digraph digraph, V source, V target) { + return digraph.contains(source, target) || Digraphs.closure(digraph, source).contains(target); + } + + /** + * Perform a depth first search. + * + * @param vertex type + * @param source dfs start vertex + * @param discovered set of vertices already discovered during search + * @param finished collection of vertices visited during search + */ + public static void dfs(Digraph digraph, V source, Set discovered, Collection finished) { + if (discovered.add(source)) { + for (V target : digraph.targets(source)) { + dfs(digraph, target, discovered, finished); + } + finished.add(source); + } + } + + /** + * Perform an undirected depth first search. + * + * @param vertex type + * @param source dfs start vertex + * @param discovered set of vertices already discovered during search + * @param finished collection of vertices visited during search + */ + public static void dfs2(Digraph digraph, V source, Set discovered, Collection finished) { + dfs2(digraph, digraph.reverse(), source, discovered, finished); + } + + private static void dfs2(Digraph forward, Digraph backward, V source, Set discovered, Collection finished) { + if (discovered.add(source)) { + for (V target : forward.targets(source)) { + dfs2(forward, backward, target, discovered, finished); + } + for (V target : backward.targets(source)) { + dfs2(forward, backward, target, discovered, finished); + } + finished.add(source); + } + } + + /** + * Compute strongly connected components. + * + * @return strongly connected components + */ + public static List> scc(Digraph digraph) { + List> components = new ArrayList<>(); + Digraph reverse = digraph.reverse(); + + // dfs on this graph + Stack stack = new Stack<>(); + Set discovered = new HashSet<>(); + for (V vertex : digraph.vertices()) { + dfs(digraph, vertex, discovered, stack); + } + + // dfs on reverse graph + discovered = new HashSet<>(); + while (!stack.isEmpty()) { + V vertex = stack.pop(); + if (!discovered.contains(vertex)) { + Set component = new HashSet<>(); + dfs(reverse, vertex, discovered, component); + components.add(component); + } + } + + return components; + } + + /** + * Compute weakly connected components. + * + * @return weakly connected components + */ + public static List> wcc(Digraph digraph) { + List> components = new ArrayList<>(); + Digraph reverse = digraph.reverse(); + + // dfs on both graphs + Set discovered = new HashSet<>(); + for (V vertex : digraph.vertices()) { + if (!discovered.contains(vertex)) { + Set component = new HashSet<>(); + dfs2(digraph, reverse, vertex, discovered, component); + components.add(component); + } + } + return components; + } + + /** + * Compute the reverse graph. + * + * @param vertex type + * @param result type + * @param digraph input de.odysseus.ithaka.digraph + * @param factory factory used to create result graph + * @return the reverse de.odysseus.ithaka.digraph + */ + public static > G reverse(Digraph digraph, DigraphFactory factory) { + G reverse = factory.create(); + for (V source : digraph.vertices()) { + reverse.add(source); + for (V target : digraph.targets(source)) { + reverse.put(target, source, digraph.get(source, target).getAsInt()); + } + } + return reverse; + } + + /** + * Copy a de.odysseus.ithaka.digraph. + * + * @param digraph graph to copy + * @param factory factory used to create copy + * @return a copy of the given de.odysseus.ithaka.digraph + */ + public static > G copy(Digraph digraph, DigraphFactory factory) { + G result = factory.create(); + for (V source : digraph.vertices()) { + result.add(source); + for (V target : digraph.targets(source)) { + result.put(source, target, digraph.get(source, target).getAsInt()); + } + } + return result; + } + + /** + * Create subgraph induced by the specified vertices. + * + * @param vertex type + * @param subgraph type + * @return subgraph of the supplied de.odysseus.ithaka.digraph containing the specified vertices. + */ + public static > G subgraph( + Digraph digraph, + Set vertices, + DigraphFactory factory) { + G subgraph = factory.create(); + for (V v : vertices) { + if (digraph.contains(v)) { + subgraph.add(v); + for (V w : digraph.targets(v)) { + if (vertices.contains(w)) { + subgraph.put(v, w, digraph.get(v, w).getAsInt()); + } + } + } + } + return subgraph; + } + + // a partition method used to be here but it was removed because it was causing issues +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraph.java b/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraph.java new file mode 100644 index 000000000..e7a337707 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraph.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +/** + * Digraph holding its reverse graph and granting access to incoming edges. + */ +public interface DoubledDigraph extends Digraph { + /** + * @return number of edges ending at vertex + */ + int getInDegree(V vertex); + + /** + * Iterate over edge sources for given target vertex. + * + * @param target target vertex + * @return edge sources of edges ending at target + */ + Iterable sources(V target); + + /** + * Restrict result type. + */ + @Override + DoubledDigraph reverse(); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraphAdapter.java b/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraphAdapter.java new file mode 100644 index 000000000..f2c1741c5 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/DoubledDigraphAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.OptionalInt; + +/** + * Doubled de.odysseus.ithaka.digraph implementation. + * + * @param vertex type + */ +public class DoubledDigraphAdapter extends DigraphAdapter implements DoubledDigraph { + /** + * Factory creating DoubledDigraph. + * + * @param factory delegate factory + * @return doubled de.odysseus.ithaka.digraph factory + */ + public static DigraphFactory> getAdapterFactory(final DigraphFactory> factory) { + return () -> new DoubledDigraphAdapter<>(factory); + } + + private final DoubledDigraphAdapter reverse; + private final DigraphFactory> factory; + + public DoubledDigraphAdapter() { + this(MapDigraph.getDefaultDigraphFactory()); + } + + public DoubledDigraphAdapter(DigraphFactory> factory) { + super(factory.create()); + this.factory = factory; + this.reverse = createReverse(); + } + + protected DoubledDigraphAdapter(DigraphFactory> factory, DoubledDigraphAdapter reverse) { + super(factory.create()); + this.factory = factory; + this.reverse = reverse; + } + + protected DoubledDigraphAdapter createReverse() { + return new DoubledDigraphAdapter<>(factory, this); + } + + protected DigraphFactory> getDigraphFactory() { + return getAdapterFactory(factory); + } + + protected DigraphFactory> getDelegateFactory() { + return factory; + } + + @Override + public int getInDegree(V vertex) { + return reverse.getOutDegree(vertex); + } + + @Override + public Iterable sources(V target) { + return reverse.targets(target); + } + + @Override + public final boolean add(V vertex) { + reverse.add0(vertex); + return add0(vertex); + } + + protected boolean add0(V vertex) { + return super.add(vertex); + } + + @Override + public final boolean remove(V vertex) { + reverse.remove0(vertex); + return remove0(vertex); + } + + protected boolean remove0(V vertex) { + return super.remove(vertex); + } + + @Override + public void removeAll(Collection vertices) { + reverse.removeAll0(vertices); + removeAll0(vertices); + } + + protected void removeAll0(Collection vertices) { + super.removeAll(vertices); + } + + /** + * Make sure the reverse de.odysseus.ithaka.digraph is kept in sync if Iterator.remove() is called. + */ + @Override + public Iterable vertices() { + final Iterator delegate = super.vertices().iterator(); + if (!delegate.hasNext()) { + return Collections.emptySet(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + V vertex; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public V next() { + return vertex = delegate.next(); + } + + @Override + public void remove() { + delegate.remove(); + reverse.remove0(vertex); + } + }; + } + + @Override + public String toString() { + return DoubledDigraphAdapter.super.vertices().toString(); + } + }; + } + + /** + * Make sure the reverse de.odysseus.ithaka.digraph is kept in sync if Iterator.remove() is called. + */ + @Override + public Iterable targets(final V source) { + final Iterator delegate = super.targets(source).iterator(); + if (!delegate.hasNext()) { + return Collections.emptySet(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + V target; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public V next() { + return target = delegate.next(); + } + + @Override + public void remove() { + delegate.remove(); + reverse.remove0(target, source); + } + }; + } + + @Override + public String toString() { + return DoubledDigraphAdapter.super.targets(source).toString(); + } + }; + } + + @Override + public final OptionalInt put(V source, V target, int edge) { + reverse.put0(target, source, edge); + return put0(source, target, edge); + } + + protected OptionalInt put0(V source, V target, int edge) { + return super.put(source, target, edge); + } + + @Override + public final OptionalInt remove(V source, V target) { + reverse.remove0(target, source); + return remove0(source, target); + } + + protected OptionalInt remove0(V source, V target) { + return super.remove(source, target); + } + + @Override + public final DoubledDigraphAdapter reverse() { + return reverse; + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/EdgeWeights.java b/src/main/java/de/odysseus/ithaka/digraph/EdgeWeights.java new file mode 100644 index 000000000..fdfc687f8 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/EdgeWeights.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.OptionalInt; + +/** + * Edge weights interface. + * + * @param vertex type + */ +public interface EdgeWeights { + // We're storing this in order to avoid reallocating this optional constantly. + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + OptionalInt UNIT_WEIGHT = OptionalInt.of(1); + + /** + * Unit edge weights. + */ + EdgeWeights UNIT_WEIGHTS = (source, target) -> UNIT_WEIGHT; + + /** + * Get edge weight + * + * @param source source vertex + * @param target target vertex + * @return weight for edge starting at source and ending at target + */ + OptionalInt get(V source, V target); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/EmptyDigraph.java b/src/main/java/de/odysseus/ithaka/digraph/EmptyDigraph.java new file mode 100644 index 000000000..28bce9269 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/EmptyDigraph.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.Collections; +import java.util.OptionalInt; +import java.util.Set; + +/** + * Empty de.odysseus.ithaka.digraph. + * Adding a vertex or edge will throw a UnsupportedOperationException. + * + * @param vertex type + */ +class EmptyDigraph implements DoubledDigraph { + @Override + public boolean add(Object vertex) { + throw new UnsupportedOperationException("Empty de.odysseus.ithaka.digraph cannot have vertices!"); + } + + @Override + public boolean contains(Object source, Object target) { + return false; + } + + @Override + public boolean contains(Object vertex) { + return false; + } + + @Override + public OptionalInt get(Object source, Object target) { + return OptionalInt.empty(); + } + + @Override + public int getInDegree(Object vertex) { + return 0; + } + + @Override + public int getOutDegree(Object vertex) { + return 0; + } + + @Override + public int getEdgeCount() { + return 0; + } + + @Override + public int getVertexCount() { + return 0; + } + + @Override + public int totalWeight() { + return 0; + } + + @Override + public Iterable vertices() { + return Collections.emptyList(); + } + + @Override + public OptionalInt put(V source, V target, int edgeWeight) { + throw new UnsupportedOperationException("Empty de.odysseus.ithaka.digraph cannot have edges!"); + } + + @Override + public OptionalInt remove(V source, V target) { + return OptionalInt.empty(); + } + + @Override + public boolean remove(Object vertex) { + return false; + } + + @Override + public void removeAll(Collection vertices) { + } + + @Override + public DoubledDigraph reverse() { + return this; + } + + @Override + public Digraph subgraph(Set vertices) { + return this; + } + + @Override + public Iterable sources(Object target) { + return Collections.emptyList(); + } + + @Override + public Iterable targets(Object source) { + return Collections.emptyList(); + } + + @Override + public boolean isAcyclic() { + return true; + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/MapDigraph.java b/src/main/java/de/odysseus/ithaka/digraph/MapDigraph.java new file mode 100644 index 000000000..4d16f5d09 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/MapDigraph.java @@ -0,0 +1,433 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.OptionalInt; +import java.util.Set; +import java.util.TreeMap; + +/** + * Map-based directed graph implementation. + * + * @param vertex type + */ +public class MapDigraph implements Digraph { + private static final int INVALID_WEIGHT = Integer.MIN_VALUE; + + /** + * Factory creating default MapDigraph. + * + * @return map de.odysseus.ithaka.digraph factory + */ + public static DigraphFactory> getDefaultDigraphFactory() { + return getMapDigraphFactory(MapDigraph.getDefaultVertexMapFactory(null), MapDigraph.getDefaultEdgeMapFactory(null)); + } + + /** + * Factory creating MapDigraph. + * + * @param vertexMapFactory factory to create vertex --> edge-map maps + * @param edgeMapFactory factory to create edge-target --> edge-value maps + * @return map de.odysseus.ithaka.digraph factory + */ + public static DigraphFactory> getMapDigraphFactory( + final VertexMapFactory vertexMapFactory, + final EdgeMapFactory edgeMapFactory) { + return () -> new MapDigraph<>(vertexMapFactory, edgeMapFactory); + } + + /** + * Vertex map factory (vertex to edge map). + */ + public interface VertexMapFactory { + Map> create(); + } + + /** + * Edge map factory (edge target to edge value). + */ + public interface EdgeMapFactory { + Object2IntMap create(V source); + } + + private static VertexMapFactory getDefaultVertexMapFactory(final Comparator comparator) { + return new VertexMapFactory() { + @Override + public Map> create() { + if (comparator == null) { + return new LinkedHashMap<>(16); + } else { + return new TreeMap<>(comparator); + } + } + }; + } + + private static EdgeMapFactory getDefaultEdgeMapFactory(final Comparator comparator) { + return new EdgeMapFactory() { + @Override + public Object2IntMap create(V ignore) { + Object2IntMap map; + + if (comparator == null) { + map = new Object2IntLinkedOpenHashMap<>(16); + } else { + map = new Object2IntAVLTreeMap<>(comparator); + } + + map.defaultReturnValue(INVALID_WEIGHT); + + return map; + } + }; + } + + private static Object2IntMap createEmptyMap() { + return Object2IntMaps.emptyMap(); + } + + private final VertexMapFactory vertexMapFactory; + private final EdgeMapFactory edgeMapFactory; + private final Map> vertexMap; + + private int edgeCount; + + /** + * Create de.odysseus.ithaka.digraph. + * {@link LinkedHashMap}s will be used as vertex/edge maps. + * Vertices and edge targets will be iterated in no particular order. + */ + public MapDigraph() { + this(null); + } + + /** + * Create de.odysseus.ithaka.digraph. + * If a vertex comparator is given, {@link TreeMap}s will be used as vertex/edge maps. + * Vertices and edge targets will be iterated in the order given by the comparator. + * + * @param comparator vertex comparator (may be null) + */ + public MapDigraph(final Comparator comparator) { + this(comparator, comparator); + } + + /** + * Create de.odysseus.ithaka.digraph. + * If a vertex comparator is given, {@link TreeMap}s will be used as vertex maps + * and vertices will be iterated in the order given by the vertex comparator. + * If an edge comparator is given, {@link TreeMap}s will be used as edge maps + * and edge targets will be iterated in the order given by the edge comparator. + */ + public MapDigraph(final Comparator vertexComparator, final Comparator edgeComparator) { + this(MapDigraph.getDefaultVertexMapFactory(vertexComparator), MapDigraph.getDefaultEdgeMapFactory(edgeComparator)); + } + + /** + * Create de.odysseus.ithaka.digraph. + * + * @param vertexMapFactory factory to create vertex --> edge-map maps + * @param edgeMapFactory factory to create edge-target --> edge-value maps + */ + public MapDigraph(VertexMapFactory vertexMapFactory, EdgeMapFactory edgeMapFactory) { + this.vertexMapFactory = vertexMapFactory; + this.edgeMapFactory = edgeMapFactory; + + vertexMap = vertexMapFactory.create(); + } + + @Override + public boolean add(V vertex) { + if (!vertexMap.containsKey(vertex)) { + vertexMap.put(vertex, createEmptyMap()); + return true; + } + + return false; + } + + @Override + public OptionalInt put(V source, V target, int weight) { + if (weight == INVALID_WEIGHT) { + throw new IllegalArgumentException("Invalid weight " + weight); + } + + Object2IntMap edgeMap = vertexMap.get(source); + + if (edgeMap == null || edgeMap.isEmpty()) { + vertexMap.put(source, edgeMap = edgeMapFactory.create(source)); + } + + int previousInt = edgeMap.put(target, weight); + OptionalInt previous; + + if (previousInt != INVALID_WEIGHT) { + previous = OptionalInt.of(previousInt); + } else { + previous = OptionalInt.empty(); + add(target); + edgeCount++; + } + + return previous; + } + + @Override + public OptionalInt get(V source, V target) { + Object2IntMap edgeMap = vertexMap.get(source); + + if (edgeMap == null || edgeMap.isEmpty()) { + return OptionalInt.empty(); + } + + int result = edgeMap.getInt(target); + + return result == INVALID_WEIGHT ? OptionalInt.empty() : OptionalInt.of(result); + } + + @Override + public OptionalInt remove(V source, V target) { + Object2IntMap edgeMap = vertexMap.get(source); + if (edgeMap == null || !edgeMap.containsKey(target)) { + return OptionalInt.empty(); + } + int result = edgeMap.removeInt(target); + edgeCount--; + if (edgeMap.isEmpty()) { + vertexMap.put(source, createEmptyMap()); + } + return result == INVALID_WEIGHT ? OptionalInt.empty() : OptionalInt.of(result); + } + + @Override + public boolean remove(V vertex) { + Object2IntMap edgeMap = vertexMap.get(vertex); + if (edgeMap == null) { + return false; + } + edgeCount -= edgeMap.size(); + vertexMap.remove(vertex); + for (V source : vertexMap.keySet()) { + remove(source, vertex); + } + return true; + } + + @Override + public void removeAll(Collection vertices) { + for (V vertex : vertices) { + Object2IntMap edgeMap = vertexMap.get(vertex); + if (edgeMap != null) { + edgeCount -= edgeMap.size(); + vertexMap.remove(vertex); + } + } + for (V source : vertexMap.keySet()) { + Object2IntMap edgeMap = vertexMap.get(source); + Iterator iterator = edgeMap.keySet().iterator(); + while (iterator.hasNext()) { + if (vertices.contains(iterator.next())) { + iterator.remove(); + edgeCount--; + } + } + if (edgeMap.isEmpty()) { + vertexMap.put(source, createEmptyMap()); + } + } + } + + @Override + public boolean contains(V source, V target) { + Object2IntMap edgeMap = vertexMap.get(source); + + if (edgeMap == null || edgeMap.isEmpty()) { + return false; + } + + return edgeMap.containsKey(target); + } + + @Override + public boolean contains(V vertex) { + return vertexMap.containsKey(vertex); + } + + @Override + public Iterable vertices() { + if (vertexMap.isEmpty()) { + return Collections.emptySet(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator delegate = vertexMap.keySet().iterator(); + V vertex = null; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public V next() { + return vertex = delegate.next(); + } + + @Override + public void remove() { + Object2IntMap edgeMap = vertexMap.get(vertex); + delegate.remove(); + edgeCount -= edgeMap.size(); + for (V source : vertexMap.keySet()) { + MapDigraph.this.remove(source, vertex); + } + } + }; + } + + @Override + public String toString() { + return vertexMap.keySet().toString(); + } + }; + } + + @Override + public Iterable targets(final V source) { + final Object2IntMap edgeMap = vertexMap.get(source); + if (edgeMap == null || edgeMap.isEmpty()) { + return Collections.emptySet(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator delegate = edgeMap.keySet().iterator(); + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public V next() { + return delegate.next(); + } + + @Override + public void remove() { + delegate.remove(); + edgeCount--; + if (edgeMap.isEmpty()) { + vertexMap.put(source, createEmptyMap()); + } + } + }; + } + + @Override + public String toString() { + return edgeMap.keySet().toString(); + } + }; + } + + @Override + public int getVertexCount() { + return vertexMap.size(); + } + + @Override + public int totalWeight() { + int weight = 0; + + for (V source : vertices()) { + for (V target : targets(source)) { + weight += get(source, target).getAsInt(); + } + } + + return weight; + } + + @Override + public int getOutDegree(V vertex) { + Object2IntMap edgeMap = vertexMap.get(vertex); + if (edgeMap == null) { + return 0; + } + return edgeMap.size(); + } + + @Override + public int getEdgeCount() { + return edgeCount; + } + + public DigraphFactory> getDigraphFactory() { + return () -> new MapDigraph<>(vertexMapFactory, edgeMapFactory); + } + + @Override + public MapDigraph reverse() { + return Digraphs.>reverse(this, getDigraphFactory()); + } + + @Override + public MapDigraph subgraph(Set vertices) { + return Digraphs.>subgraph(this, vertices, getDigraphFactory()); + } + + @Override + public boolean isAcyclic() { + return Digraphs.isAcyclic(this); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append(getClass().getName().substring(getClass().getName().lastIndexOf('.') + 1)); + b.append("("); + Iterator vertices = vertices().iterator(); + while (vertices.hasNext()) { + V v = vertices.next(); + b.append(v); + b.append(targets(v)); + if (vertices.hasNext()) { + b.append(", "); + if (b.length() > 1000) { + b.append("..."); + break; + } + } + } + b.append(")"); + return b.toString(); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/TrivialDigraph.java b/src/main/java/de/odysseus/ithaka/digraph/TrivialDigraph.java new file mode 100644 index 000000000..a0e7da537 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/TrivialDigraph.java @@ -0,0 +1,253 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.OptionalInt; +import java.util.Set; + +/** + * Convenience class representing a de.odysseus.ithaka.digraph with zero or one vertex and an optional loop edge. + * Vertex as well as edge null is forbidden. + * + * @param vertex type + * @author beck + */ +public class TrivialDigraph implements DoubledDigraph { + private V vertex; + private boolean hasLoop; + private int loopWeight; + + public TrivialDigraph() { + vertex = null; + hasLoop = false; + loopWeight = 0; + } + + /** + * @throws UnsupportedOperationException if adding the vertex would result in having 2 vertices in the graph + * @throws IllegalArgumentException if vertex == null + */ + @Override + public boolean add(V vertex) { + if (vertex == null) { + throw new IllegalArgumentException("Cannot add null vertex!"); + } + + if (this.vertex == null) { + this.vertex = vertex; + return true; + } + + if (this.vertex.equals(vertex)) { + return false; + } + + throw new UnsupportedOperationException("TrivialDigraph must contain at most one vertex!"); + } + + @Override + public boolean contains(Object source, Object target) { + return vertex != null && hasLoop && vertex.equals(source) && vertex.equals(target); + } + + @Override + public boolean contains(Object vertex) { + return this.vertex != null && this.vertex.equals(vertex); + } + + @Override + public OptionalInt get(Object source, Object target) { + return contains(source, target) ? OptionalInt.of(loopWeight) : OptionalInt.empty(); + } + + @Override + public int getInDegree(Object vertex) { + return hasLoop ? 1 : 0; + } + + @Override + public int getOutDegree(Object vertex) { + return hasLoop ? 1 : 0; + } + + @Override + public int getEdgeCount() { + return hasLoop ? 1 : 0; + } + + @Override + public int getVertexCount() { + return vertex == null ? 0 : 1; + } + + @Override + public int totalWeight() { + return hasLoop ? loopWeight : 0; + } + + @Override + public Iterable vertices() { + if (vertex == null) { + return Collections.emptyList(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + boolean hasNext = true; + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public V next() { + if (hasNext) { + hasNext = false; + return vertex; + } + throw new NoSuchElementException("No more vertices"); + } + + @Override + public void remove() { + if (hasNext) { + throw new IllegalStateException(); + } + TrivialDigraph.this.remove(vertex); + } + }; + } + + @Override + public String toString() { + return "[" + vertex + "]"; + } + }; + } + + @Override + public OptionalInt put(V source, V target, int loopWeight) { + if (source != target) { + throw new UnsupportedOperationException("TrivialDigraph must not contain no-loop edges!"); + } + + OptionalInt previousLoopWeight = hasLoop ? OptionalInt.of(this.loopWeight) : OptionalInt.empty(); + add(source); + this.hasLoop = true; + this.loopWeight = loopWeight; + + return previousLoopWeight; + } + + @Override + public OptionalInt remove(V source, V target) { + if (contains(source, target)) { + int loopWeight = this.loopWeight; + this.loopWeight = 0; + this.hasLoop = false; + return OptionalInt.of(loopWeight); + } + + return OptionalInt.empty(); + } + + @Override + public boolean remove(V vertex) { + if (this.vertex != null && this.vertex.equals(vertex)) { + this.vertex = null; + this.loopWeight = 0; + this.hasLoop = false; + return true; + } + + return false; + } + + @Override + public void removeAll(Collection vertices) { + if (vertices.contains(vertex)) { + remove(vertex); + } + } + + @Override + public DoubledDigraph reverse() { + return this; + } + + @Override + public Digraph subgraph(Set vertices) { + return vertex != null && vertices.contains(vertex) ? this : Digraphs.emptyDigraph(); + } + + @Override + public Iterable sources(Object target) { + return targets(target); + } + + @Override + public Iterable targets(Object source) { + if (!hasLoop || vertex == null || !vertex.equals(source)) { + return Collections.emptyList(); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + boolean hasNext = true; + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public V next() { + if (hasNext) { + hasNext = false; + return vertex; + } + throw new NoSuchElementException("No more vertices"); + } + + @Override + public void remove() { + if (hasNext) { + throw new IllegalStateException(); + } + TrivialDigraph.this.remove(vertex, vertex); + } + }; + } + + @Override + public String toString() { + return "[" + vertex + "]"; + } + }; + } + + @Override + public boolean isAcyclic() { + return !hasLoop; + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/UnmodifiableDigraph.java b/src/main/java/de/odysseus/ithaka/digraph/UnmodifiableDigraph.java new file mode 100644 index 000000000..29b309a32 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/UnmodifiableDigraph.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph; + +import java.util.Collection; +import java.util.OptionalInt; + +/** + * Unmodifiable de.odysseus.ithaka.digraph adapter. + * Overrides add, put, remove, removeAll to throw an exception. + * + * @param vertex type + */ +public class UnmodifiableDigraph extends DigraphAdapter { + public UnmodifiableDigraph(Digraph digraph) { + super(digraph); + } + + /** + * @throws UnsupportedOperationException unmodifiable + */ + @Override + public final boolean add(V vertex) { + throw new UnsupportedOperationException("This de.odysseus.ithaka.digraph is readonly!"); + } + + /** + * @throws UnsupportedOperationException unmodifiable + */ + @Override + public final OptionalInt put(V source, V target, int edge) { + throw new UnsupportedOperationException("This de.odysseus.ithaka.digraph is readonly!"); + } + + /** + * @throws UnsupportedOperationException unmodifiable + */ + @Override + public final boolean remove(V vertex) { + throw new UnsupportedOperationException("This de.odysseus.ithaka.digraph is readonly!"); + } + + /** + * @throws UnsupportedOperationException unmodifiable + */ + @Override + public final OptionalInt remove(V source, V target) { + throw new UnsupportedOperationException("This de.odysseus.ithaka.digraph is readonly!"); + } + + /** + * @throws UnsupportedOperationException unmodifiable + */ + @Override + public final void removeAll(Collection vertices) { + throw new UnsupportedOperationException("This de.odysseus.ithaka.digraph is readonly!"); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotAttribute.java b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotAttribute.java new file mode 100644 index 000000000..96e05a9bd --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotAttribute.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.io.dot; + +import java.awt.Color; +import java.io.IOException; +import java.io.Writer; + +public class DotAttribute { + private static boolean isIdentifier(String value) { + if (!Character.isJavaIdentifierStart(value.charAt(0))) { + return false; + } + for (char c : value.substring(1).toCharArray()) { + if (!Character.isJavaIdentifierPart(c)) { + return false; + } + } + return true; + } + + private final String name; + private final String value; + private final boolean quotes; + + public DotAttribute(String name, String value) { + this.name = name; + this.value = value; + this.quotes = !isIdentifier(value); + } + + public DotAttribute(String name, Number value) { + this.name = name; + this.value = value.toString(); + this.quotes = false; + } + + public DotAttribute(String name, boolean value) { + this.name = name; + this.value = String.valueOf(value); + this.quotes = false; + } + + public DotAttribute(String name, Color value) { + this.name = name; + this.value = String.format("#%6X", value.getRGB() & 0x00FFFFFF); + this.quotes = true; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void write(Writer writer) throws IOException { + writer.write(name); + writer.write('='); + if (quotes) { + writer.write('"'); + } + writer.write(value); + if (quotes) { + writer.write('"'); + } + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotExporter.java b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotExporter.java new file mode 100644 index 000000000..83e2cbcc1 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotExporter.java @@ -0,0 +1,224 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.io.dot; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.DigraphProvider; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class DotExporter { + private static class Cluster> { + String id; + G subgraph; + V sample; + DotAttribute tail; + DotAttribute head; + + public Cluster(String id, G subgraph) { + this.id = id; + this.subgraph = subgraph; + this.sample = subgraph.vertices().iterator().next(); + + this.head = new DotAttribute("lhead", id); + this.tail = new DotAttribute("ltail", id); + } + } + + private final String indent; + private final String lineSpeparator; + + public DotExporter() { + this(" ", System.getProperty("line.separator")); + } + + public DotExporter(String indent, String newline) { + this.indent = indent; + this.lineSpeparator = newline; + } + + private void indent(Writer writer, int level) throws IOException { + for (int i = 0; i < level; i++) { + writer.write(indent); + } + } + + private void writeAttributes(Writer writer, Iterator iterator) throws IOException { + if (iterator.hasNext()) { + boolean first = true; + while (iterator.hasNext()) { + if (first) { + writer.write('['); + first = false; + } else { + writer.write(", "); + } + iterator.next().write(writer); + } + writer.write(']'); + } + } + + private void writeDefaultAttributes(Writer writer, int level, String name, Iterable attributes) throws IOException { + if (attributes != null) { + indent(writer, level); + Iterator iterator = attributes.iterator(); + if (iterator.hasNext()) { + writer.write(name); + writeAttributes(writer, iterator); + } + writer.write(";"); + writer.write(lineSpeparator); + } + } + + private void writeNode(Writer writer, int level, V vertex, DotProvider provider) throws IOException { + indent(writer, level); + writer.write(provider.getNodeId(vertex)); + Iterable attributes = provider.getNodeAttributes(vertex); + if (attributes != null) { + writeAttributes(writer, attributes.iterator()); + } + writer.write(";"); + writer.write(lineSpeparator); + } + + private void writeEdge(Writer writer, int level, V source, V target, int edgeWeight, DotProvider provider, + Cluster sourceCluster, Cluster targetCluster) throws IOException { + indent(writer, level); + writer.write(provider.getNodeId(sourceCluster == null ? source : sourceCluster.sample)); + writer.write(" -> "); + writer.write(provider.getNodeId(targetCluster == null ? target : targetCluster.sample)); + Iterable attributes = provider.getEdgeAttributes(source, target, edgeWeight); + if (sourceCluster == null && targetCluster == null) { + if (attributes != null) { + writeAttributes(writer, attributes.iterator()); + } + } else { + List attributeList = new ArrayList<>(); + if (sourceCluster != null) { + attributeList.add(sourceCluster.tail); + } + if (targetCluster != null) { + attributeList.add(targetCluster.head); + } + if (attributes != null) { + for (DotAttribute attribute : attributes) { + attributeList.add(attribute); + } + } + writeAttributes(writer, attributeList.iterator()); + } + writer.write(";"); + writer.write(lineSpeparator); + } + + private > Map> createClusters( + G digraph, + DotProvider provider, + DigraphProvider subgraphs) { + Map> clusters = new HashMap<>(); + if (subgraphs != null) { + for (V vertex : digraph.vertices()) { + G subgraph = subgraphs.get(vertex); + if (subgraph != null && subgraph.getVertexCount() > 0) { + clusters.put(vertex, new Cluster<>("cluster_" + provider.getNodeId(vertex), subgraph)); + } + } + } + return clusters; + } + + public > void export( + DotProvider provider, + G digraph, + DigraphProvider subgraphs, + Writer writer) throws IOException { + + writer.write("de.odysseus.ithaka.digraph G {"); + writer.write(lineSpeparator); + + Map> clusters = createClusters(digraph, provider, subgraphs); + if (!clusters.isEmpty()) { + indent(writer, 1); + writer.write("compound=true;"); + writer.write(lineSpeparator); + } + + writeDefaultAttributes(writer, 1, "graph", provider.getDefaultGraphAttributes(digraph)); + writeDefaultAttributes(writer, 1, "node", provider.getDefaultNodeAttributes(digraph)); + writeDefaultAttributes(writer, 1, "edge", provider.getDefaultEdgeAttributes(digraph)); + + writeNodesAndEdges(writer, 1, provider, digraph, clusters, subgraphs); + + writer.write("}"); + writer.write(lineSpeparator); + + writer.flush(); + } + + private > void writeNodesAndEdges( + Writer writer, + int level, + DotProvider provider, + G digraph, + Map> clusters, + DigraphProvider subgraphs) throws IOException { + for (V vertex : digraph.vertices()) { + if (clusters.containsKey(vertex)) { + writeCluster(writer, level, provider, vertex, clusters.get(vertex), subgraphs); + } else { + writeNode(writer, level, vertex, provider); + } + } + for (V source : digraph.vertices()) { + for (V target : digraph.targets(source)) { + writeEdge(writer, level, source, target, digraph.get(source, target).getAsInt(), provider, + clusters.get(source), clusters.get(target)); + } + } + } + + private > void writeCluster( + Writer writer, + int level, + DotProvider provider, + V subgraphVertex, + Cluster cluster, + DigraphProvider subgraphs) throws IOException { + + indent(writer, level); + writer.write("subgraph "); + writer.write(cluster.id); + writer.write(" {"); + writer.write(lineSpeparator); + + writeDefaultAttributes(writer, level + 1, "graph", provider.getSubgraphAttributes(cluster.subgraph, subgraphVertex)); + + Map> subclusters = createClusters(cluster.subgraph, provider, subgraphs); + writeNodesAndEdges(writer, level + 1, provider, cluster.subgraph, subclusters, subgraphs); + + indent(writer, level); + writer.write("}"); + writer.write(lineSpeparator); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotProvider.java b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotProvider.java new file mode 100644 index 000000000..65e85ca47 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/io/dot/DotProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.io.dot; + +import de.odysseus.ithaka.digraph.Digraph; + +public interface DotProvider> { + Iterable getDefaultGraphAttributes(G digraph); + + Iterable getDefaultNodeAttributes(G digraph); + + Iterable getDefaultEdgeAttributes(G digraph); + + String getNodeId(V vertex); + + Iterable getNodeAttributes(V vertex); + + Iterable getEdgeAttributes(V source, V target, int edgeWeight); + + Iterable getSubgraphAttributes(G subgraph, V vertex); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfExporter.java b/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfExporter.java new file mode 100644 index 000000000..d381f5338 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfExporter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.io.tgf; + +import de.odysseus.ithaka.digraph.Digraph; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + +public class TgfExporter { + private final String newline; + + public TgfExporter() { + this(System.getProperty("line.separator")); + } + + public TgfExporter(String newline) { + this.newline = newline; + } + + public void export( + TgfLabelProvider provider, + Digraph digraph, + Writer writer) throws IOException { + Map index = new HashMap<>(); + int n = 0; + + for (V vertex : digraph.vertices()) { + n += 1; + index.put(vertex, n); + writer.write(String.valueOf(n)); + String label = provider.getVertexLabel(vertex); + if (label != null) { + writer.write(' '); + writer.write(label); + } + writer.write(newline); + } + + writer.write('#'); + writer.write(newline); + + for (V source : digraph.vertices()) { + for (V target : digraph.targets(source)) { + writer.write(String.valueOf(index.get(source))); + writer.write(' '); + writer.write(String.valueOf(index.get(target))); + String label = provider.getEdgeLabel(digraph.get(source, target).getAsInt()); + if (label != null) { + writer.write(' '); + writer.write(label); + } + writer.write(newline); + } + } + + writer.flush(); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfLabelProvider.java b/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfLabelProvider.java new file mode 100644 index 000000000..539db91c8 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/io/tgf/TgfLabelProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.io.tgf; + +public interface TgfLabelProvider { + String getVertexLabel(V vertex); + + String getEdgeLabel(int edgeWeight); +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/util/fas/AbstractFeedbackArcSetProvider.java b/src/main/java/de/odysseus/ithaka/digraph/util/fas/AbstractFeedbackArcSetProvider.java new file mode 100644 index 000000000..9fc5640e5 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/util/fas/AbstractFeedbackArcSetProvider.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.util.fas; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.Digraphs; +import de.odysseus.ithaka.digraph.EdgeWeights; +import de.odysseus.ithaka.digraph.MapDigraph; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Abstract feedback arc set provider. + */ +public abstract class AbstractFeedbackArcSetProvider implements FeedbackArcSetProvider { + class FeedbackTask implements Callable> { + final Digraph digraph; + final EdgeWeights weights; + final FeedbackArcSetPolicy policy; + final Set scc; + + FeedbackTask(Digraph digraph, EdgeWeights weights, FeedbackArcSetPolicy policy, Set scc) { + this.digraph = digraph; + this.weights = weights; + this.policy = policy; + this.scc = scc; + } + + @Override + public FeedbackArcSet call() { + return fas(digraph.subgraph(scc), weights, policy); + } + } + + private final ExecutorService executor; + + /** + * Create provider which calculates a feedback arc set on a de.odysseus.ithaka.digraph (in the + * current thread). + * + * The provider decomposes a de.odysseus.ithaka.digraph into strongly connected components and computes + * feedback arc sets on the components and combines the results. + * + * The {@link #mfas(Digraph, EdgeWeights)} and {@link #lfas(Digraph, EdgeWeights)} + * implementation methods do not have to handle arbitrary digraphs for this reason. + */ + protected AbstractFeedbackArcSetProvider() { + this.executor = null; + } + + /** + * Create provider which decomposes a de.odysseus.ithaka.digraph into strongly connected components + * and computes feedback arc sets on the components and combines the results. + * Feedback calculations can be distributed to a given number of threads. + * If numberOfThreads == 0, calculation is done in the current thread. + * + * @param numberOfThreads number + */ + protected AbstractFeedbackArcSetProvider(int numberOfThreads) { + if (numberOfThreads > 0) { + this.executor = Executors.newFixedThreadPool(numberOfThreads); + } else { + this.executor = null; + } + } + + /** + * Compute minimum feedback arc set. + * + * @return feedback arc set or null + */ + protected Digraph mfas(Digraph digraph, EdgeWeights weights) { + return null; + } + + /** + * Compute light feedback arc set. + * + * @param digraph original graph or tangle of it (if decompose == true) + * @return feedback arc set + */ + protected abstract Digraph lfas(Digraph digraph, EdgeWeights weights); + + private FeedbackArcSet fas(Digraph digraph, EdgeWeights weights, FeedbackArcSetPolicy policy) { + EdgeWeights filteredWeights = weights; + if (policy == FeedbackArcSetPolicy.MIN_SIZE) { + /* + * Manipulate graph weights if the feedback arc set has to be of minimum size (i.e., #arcs): + * all weights are increased by an amount (delta) equal to the sum of all weights, + * so that every arc is heavier than any arc subset with the original weights. + * A minimum weight feedback arc set (mwfas) of the resulting graph has a total weight of + * #arcs(mwfas) * delta + origWeight(mwfas). + * Since origWeight(mwfas) < delta, the determined mwfas has a minimum #arcs and + * from all those feedback arc sets of minimum size it has minimum original weight (we could + * have obtained the first result easily by setting all weights to 1, but not the second). + */ + final EdgeWeights origWeights = weights; + final int delta = totalWeight(digraph, origWeights); + filteredWeights = new EdgeWeights() { + @Override + public OptionalInt get(V source, V target) { + OptionalInt original = origWeights.get(source, target); + + if (original.isPresent()) { + return OptionalInt.of(original.getAsInt() + delta); + } else { + return OptionalInt.empty(); + } + } + }; + } + Digraph result = mfas(digraph, filteredWeights); + boolean exact = true; + if (result == null) { + result = lfas(digraph, filteredWeights); + exact = false; + } + return new FeedbackArcSet<>(result, totalWeight(result, weights), policy, exact); + } + + protected int totalWeight(Digraph digraph, EdgeWeights weights) { + int weight = 0; + for (V source : digraph.vertices()) { + for (V target : digraph.targets(source)) { + weight += weights.get(source, target).getAsInt(); + } + } + return weight; + } + + private List> executeAll(List> tasks) { + List> result = new ArrayList<>(); + + if (executor == null) { + for (FeedbackTask task : tasks) { + result.add(task.call()); + } + } else { + try { + for (Future> future : executor.invokeAll(tasks)) { + result.add(future.get()); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + return null; // should not happen + } + } + + return result; + } + + @Override + public FeedbackArcSet getFeedbackArcSet(Digraph digraph, EdgeWeights weights, FeedbackArcSetPolicy policy) { + if (Digraphs.isTriviallyAcyclic(digraph)) { + // known acyclic based on low vertex count + return FeedbackArcSet.empty(policy); + } + + List> components = Digraphs.scc(digraph); + + if (components.size() == digraph.getVertexCount()) { + // known acyclic based on strongly connected components + return FeedbackArcSet.empty(policy); + } + + if (components.size() == 1) { + return fas(digraph, weights, policy); + } + + List> tasks = new ArrayList<>(); + + for (Set component : components) { + if (component.size() > 1) { + tasks.add(new FeedbackTask<>(digraph, weights, policy, component)); + } + } + + List> feedbacks = executeAll(tasks); + if (feedbacks == null) { + return null; + } + + int weight = 0; + boolean exact = true; + + Digraph result = new MapDigraph<>(); + for (FeedbackArcSet feedback : feedbacks) { + for (V source : feedback.vertices()) { + for (V target : feedback.targets(source)) { + result.put(source, target, digraph.get(source, target).getAsInt()); + } + } + exact &= feedback.isExact(); + weight += feedback.getWeight(); + } + + return new FeedbackArcSet<>(result, weight, policy, exact); + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSet.java b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSet.java new file mode 100644 index 000000000..cdaaa02b4 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSet.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.util.fas; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.Digraphs; +import de.odysseus.ithaka.digraph.UnmodifiableDigraph; + +/** + * Feedback arc set. + * + * @param vertex type + */ +public class FeedbackArcSet extends UnmodifiableDigraph { + private final FeedbackArcSetPolicy policy; + private final boolean exact; + private final int weight; + + public FeedbackArcSet(Digraph feedback, int weight, FeedbackArcSetPolicy policy, boolean exact) { + super(feedback); + this.weight = weight; + this.policy = policy; + this.exact = exact; + } + + public static FeedbackArcSet empty(FeedbackArcSetPolicy policy) { + return new FeedbackArcSet<>(Digraphs.emptyDigraph(), 0, policy, true); + } + + /** + * @return true if this FAS is known to be of minimal + */ + public boolean isExact() { + return exact; + } + + /** + * @return total weight + */ + public int getWeight() { + return weight; + } + + /** + * @return minimization policy (weight/#arcs) + */ + public FeedbackArcSetPolicy getPolicy() { + return policy; + } +} diff --git a/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetPolicy.java b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetPolicy.java new file mode 100644 index 000000000..51de76d61 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetPolicy.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.util.fas; + +/** + * Minimization policy + */ +public enum FeedbackArcSetPolicy { + /** + * minimize number of feedback arcs + */ + MIN_SIZE, + + /** + * minimize weight of feedback arcs + */ + MIN_WEIGHT +} \ No newline at end of file diff --git a/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetProvider.java b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetProvider.java new file mode 100644 index 000000000..c9b89cfd5 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/util/fas/FeedbackArcSetProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.util.fas; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.EdgeWeights; + +/** + * Feedback arc set provider interface. + */ +public interface FeedbackArcSetProvider { + /** + * Calculate feedback arc set. + * + * @return feedback arc set + */ + FeedbackArcSet getFeedbackArcSet(Digraph digraph, + EdgeWeights weights, + FeedbackArcSetPolicy policy); +} \ No newline at end of file diff --git a/src/main/java/de/odysseus/ithaka/digraph/util/fas/SimpleFeedbackArcSetProvider.java b/src/main/java/de/odysseus/ithaka/digraph/util/fas/SimpleFeedbackArcSetProvider.java new file mode 100644 index 000000000..53ccd9268 --- /dev/null +++ b/src/main/java/de/odysseus/ithaka/digraph/util/fas/SimpleFeedbackArcSetProvider.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012 Odysseus Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.odysseus.ithaka.digraph.util.fas; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.DigraphFactory; +import de.odysseus.ithaka.digraph.Digraphs; +import de.odysseus.ithaka.digraph.EdgeWeights; +import de.odysseus.ithaka.digraph.MapDigraph; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +/** + * Simple feedback arc set provider. + */ +public class SimpleFeedbackArcSetProvider extends AbstractFeedbackArcSetProvider { + /** + * Calculate feedback arc in the current thread. + */ + public SimpleFeedbackArcSetProvider() { + super(); + } + + /** + * Calculate feedback arc set using the specified number of threads. + */ + public SimpleFeedbackArcSetProvider(int numberOfThreads) { + super(numberOfThreads); + } + + /** + * create equivalent graphs with different edge orderings. + * + * @param digraph de.odysseus.ithaka.digraph to copy + * @return list of copies + */ + private List> copies(Digraph digraph, int count) { + List> copies = new ArrayList<>(); + copies.add(digraph); + + final List shuffle = new ArrayList<>(); + final Map order = new HashMap<>(); + int index = 0; + for (V source : digraph.vertices()) { + order.put(source, index); + shuffle.add(index++); + } + + Random random = new Random(7); + for (int i = 0; i < count; i++) { + Collections.shuffle(shuffle, random); + List mapping = new ArrayList<>(shuffle); + + copies.add(Digraphs.copy(digraph, new DigraphFactory>() { + @Override + public Digraph create() { + return new MapDigraph<>(new Comparator() { + @Override + public int compare(V v1, V v2) { + int value1 = mapping.get(order.get(v1)); + int value2 = mapping.get(order.get(v2)); + return Integer.compare(value1, value2); + } + }); + } + })); + } + return copies; + } + + /** + * Compute simple feedback arc set by performing |n| DFS traversals (each starting + * with a different vertex) on the tangle, taking non-forward edges as feedback. + * The minimum weight feedback arc set among those |n| results is returned. + * + * @param tangle strongly connected component + * @param weights edge weights + * @return feedback arc set + */ + @Override + protected Digraph lfas(Digraph tangle, EdgeWeights weights) { + /* + * store best results + */ + int minWeight = Integer.MAX_VALUE; + int minSize = Integer.MAX_VALUE; + List minFinished = null; + + /* + * threshold on max. number of iterations (avoid running forever) + */ + int maxIterationsLeft = Math.max(1, 1000000 / (tangle.getVertexCount() + tangle.getEdgeCount())); + + /* + * perform DFS for each node, keep best result + */ + List> copies = copies(tangle, Math.min(10, tangle.getVertexCount())); + List finished = new ArrayList<>(tangle.getVertexCount()); + Set discovered = new HashSet<>(tangle.getVertexCount()); + for (V start : tangle.vertices()) { + for (Digraph copy : copies) { + finished.clear(); + discovered.clear(); + Digraphs.dfs(copy, start, discovered, finished); + assert finished.size() == tangle.getVertexCount(); + + int weight = 0; + int size = 0; + discovered.clear(); + for (V source : finished) { + discovered.add(source); + for (V target : tangle.targets(source)) { + if (!discovered.contains(target)) { // feedback edge + weight += weights.get(source, target).getAsInt(); + size++; + } + } + if (weight > minWeight) { + break; + } + } + if (weight < minWeight || weight == minWeight && size < minSize) { + minFinished = new ArrayList<>(finished); + minWeight = weight; + minSize = size; + } + } + if (--maxIterationsLeft == 0) { + break; + } + } + + // If the input graph has at least a single vertex, we'll get at least one result. + Objects.requireNonNull(minFinished); + + /* + * create feedback graph + */ + Digraph feedback = MapDigraph.getDefaultDigraphFactory().create(); + discovered.clear(); + for (V source : minFinished) { + discovered.add(source); + for (V target : tangle.targets(source)) { + if (!discovered.contains(target)) { // feedback edge + feedback.put(source, target, tangle.get(source, target).getAsInt()); + } + } + } + + return feedback; + } +} diff --git a/src/main/java/jss/notfine/NotFine.java b/src/main/java/jss/notfine/NotFine.java new file mode 100644 index 000000000..babd270f3 --- /dev/null +++ b/src/main/java/jss/notfine/NotFine.java @@ -0,0 +1,44 @@ +package jss.notfine; + +import com.gtnewhorizons.angelica.Tags; +import cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import jss.notfine.proxy.CommonProxy; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod( + modid = NotFine.MODID, + name = NotFine.NAME, + version = NotFine.VERSION, + acceptableRemoteVersions = "*" +) +public class NotFine { + public static final String MODID = "notfine"; + public static final String NAME = "NotFine"; + public static final String VERSION = Tags.VERSION; + public static final Logger logger = LogManager.getLogger(NAME); + + @SidedProxy(clientSide = "jss.notfine.proxy.ClientProxy", serverSide = "jss.notfine.proxy.CommonProxy") + public static CommonProxy proxy; + + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + proxy.preInit(event); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + proxy.init(event); + } + + @Mod.EventHandler + public void postInit(FMLPostInitializationEvent event) { + proxy.postInit(event); + } + +} diff --git a/src/main/java/jss/notfine/config/NotFineConfig.java b/src/main/java/jss/notfine/config/NotFineConfig.java new file mode 100644 index 000000000..44ddd1723 --- /dev/null +++ b/src/main/java/jss/notfine/config/NotFineConfig.java @@ -0,0 +1,30 @@ +package jss.notfine.config; + +import net.minecraft.launchwrapper.Launch; +import net.minecraftforge.common.config.Configuration; +import jss.notfine.NotFine; + +import java.io.File; + +public class NotFineConfig { + + public static boolean allowAdvancedOpenGL = true; + + public static final String CATEGORY_CLIENT = "client"; + + private final Configuration notFineConfig; + + public NotFineConfig() { + File configFile = new File(Launch.minecraftHome + File.separator + "config" + File.separator + NotFine.MODID + File.separator + "notfine.cfg"); + notFineConfig = new Configuration(configFile); + } + + public void loadSettings() { + allowAdvancedOpenGL = notFineConfig.getBoolean("allowAdvancedOpenGL", CATEGORY_CLIENT, true, "Allow or always remove the Advanced OpenGL button"); + + if(notFineConfig.hasChanged()) { + notFineConfig.save(); + } + } + +} diff --git a/src/main/java/jss/notfine/config/VideoSettings.java b/src/main/java/jss/notfine/config/VideoSettings.java new file mode 100644 index 000000000..06b666847 --- /dev/null +++ b/src/main/java/jss/notfine/config/VideoSettings.java @@ -0,0 +1,58 @@ +package jss.notfine.config; + +import cpw.mods.fml.client.FMLClientHandler; +import jss.notfine.NotFine; +import jss.notfine.core.Settings; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; + +public class VideoSettings { + private final File optionsFile; + + public VideoSettings(File optionsFile) { + this.optionsFile = optionsFile; + } + + public void loadSettings() { + try { + if (!optionsFile.exists()) { + return; + } + BufferedReader bufferedreader = new BufferedReader(new FileReader(optionsFile)); + String settingString; + + while((settingString = bufferedreader.readLine()) != null) { + try { + String[] fragments = settingString.split(":"); + Settings setting = Settings.valueOf(fragments[0]); + setting.option.deserialize(fragments[1]); + setting.applyChanges(); + } catch (Exception exception) { + NotFine.logger.warn("Skipping bad option: " + settingString); + } + + } + bufferedreader.close(); + } catch (Exception exception) { + NotFine.logger.error("Failed to load options", exception); + } + } + + public void saveSettings() { + if (FMLClientHandler.instance().isLoading()) return; + try { + PrintWriter printwriter = new PrintWriter(new FileWriter(optionsFile)); + for(Settings setting : Settings.values()) { + printwriter.println(setting.name() + ':' + setting.option.getStore()); + } + printwriter.close(); + } catch(Exception exception) { + NotFine.logger.error("Failed to save options", exception); + } + } + +} diff --git a/src/main/java/jss/notfine/core/Settings.java b/src/main/java/jss/notfine/core/Settings.java new file mode 100644 index 000000000..cd07bd4d0 --- /dev/null +++ b/src/main/java/jss/notfine/core/Settings.java @@ -0,0 +1,285 @@ +package jss.notfine.core; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import jss.notfine.gui.options.control.NotFineControlValueFormatter; +import jss.notfine.gui.options.named.AlwaysNever; +import jss.notfine.gui.options.named.BackgroundSelect; +import jss.notfine.gui.options.named.DownfallQuality; +import jss.notfine.gui.options.named.GraphicsQualityOff; +import jss.notfine.gui.options.named.LeavesQuality; +import jss.notfine.gui.options.named.GraphicsToggle; +import jss.notfine.render.RenderStars; +import jss.notfine.gui.options.storage.NotFineMinecraftOptionsStorage; +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpact; +import me.jellysquid.mods.sodium.client.gui.options.control.Control; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl; +import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl; +import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.MathHelper; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; + +@SideOnly(Side.CLIENT) +public enum Settings { + CLOUD_HEIGHT(new NotFineOptionSlider(128, 96, 384, 8, null)) { + @Override + public void applyChanges() { + SettingsManager.cloudsUpdated(); + } + }, + CLOUD_SCALE(new NotFineOptionSlider(4, 2, 20, 1, null)), + DOWNFALL_DISTANCE(new NotFineOptionCycling<>(DownfallQuality.DEFAULT, OptionImpact.MEDIUM)) { + @Override + public void applyChanges() { + SettingsManager.downfallDistanceUpdated(); + } + }, + GUI_BACKGROUND(new NotFineOptionCycling<>(BackgroundSelect.DEFAULT, null)) { + @Override + public void applyChanges() { + SettingsManager.backgroundUpdated(); + } + }, + MODE_CLOUD_TRANSLUCENCY(new NotFineOptionCycling<>(AlwaysNever.DEFAULT, null)) { + @Override + public void applyChanges() { + SettingsManager.cloudsUpdated(); + } + }, + MODE_CLOUDS(new NotFineOptionCycling<>(GraphicsQualityOff.DEFAULT, OptionImpact.MEDIUM)) { + @Override + public void applyChanges() { + SettingsManager.cloudsUpdated(); + } + }, + MODE_DROPPED_ITEMS(new NotFineOptionCycling<>(GraphicsQuality.DEFAULT, OptionImpact.LOW)) { + @Override + public void applyChanges() { + SettingsManager.droppedItemDetailUpdated(); + } + }, + MODE_GLINT_INV(new NotFineOptionTickBox(true, OptionImpact.VARIES)), + MODE_GLINT_WORLD(new NotFineOptionTickBox(true, OptionImpact.VARIES)), + MODE_GUI_BACKGROUND(new NotFineOptionTickBox(true, null)), + MODE_LEAVES(new NotFineOptionCycling<>(LeavesQuality.DEFAULT, OptionImpact.VARIES, OptionFlag.REQUIRES_RENDERER_RELOAD)) { + @Override + public void applyChanges() { + SettingsManager.leavesUpdated(); + } + }, + MODE_SHADOWS(new NotFineOptionCycling<>(GraphicsToggle.DEFAULT, OptionImpact.LOW)) { + @Override + public void applyChanges() { + SettingsManager.shadowsUpdated(); + } + }, + MODE_SKY(new NotFineOptionTickBox(true, OptionImpact.MEDIUM)), + MODE_WATER(new NotFineOptionCycling<>(GraphicsQuality.DEFAULT, OptionImpact.LOW)) { + @Override + public void applyChanges() { + SettingsManager.waterDetailUpdated(); + } + }, + MODE_VIGNETTE(new NotFineOptionCycling<>(GraphicsToggle.DEFAULT, OptionImpact.LOW)) { + @Override + public void applyChanges() { + SettingsManager.vignetteUpdated(); + } + }, + PARTICLES_ENC_TABLE(new NotFineOptionSlider(1, 0, 16, 1, OptionImpact.LOW)), + PARTICLES_VOID(new NotFineOptionTickBox(true, OptionImpact.LOW)), + RENDER_DISTANCE_CLOUDS(new NotFineOptionSlider(4, 4, 64, 2, OptionImpact.VARIES)) { + @Override + public void applyChanges() { + SettingsManager.cloudsUpdated(); + } + }, + TOTAL_STARS(new NotFineOptionSlider(1500, 0, 32000, 500, OptionImpact.LOW)) { + @Override + public void applyChanges() { + RenderStars.reloadStarRenderList(Minecraft.getMinecraft().renderGlobal); + } + }; + + public final NotFineOption option; + + Settings(NotFineOption option) { + this.option = option; + } + + public void ready() { + option.setting = this; + } + + public void applyChanges() { + + } + + public static class NotFineOptionCycling> extends NotFineOption { + + protected NotFineOptionCycling(T base, OptionImpact impact, OptionFlag... optionFlags) { + super(base, impact, optionFlags); + } + + @Override + public void deserialize(String fragment) { + store = T.valueOf(value.getDeclaringClass(), fragment); + value = store; + modifiedValue = store; + } + + @Override + public Control getControl() { + return new CyclingControl<>(this, value.getDeclaringClass()); + } + + } + + public static class NotFineOptionSlider extends NotFineOption { + public final int min, max, step; + + protected NotFineOptionSlider(int base, int min, int max, int step, OptionImpact impact, OptionFlag... optionFlags) { + super(base, impact, optionFlags); + this.min = min; + this.max = max; + this.step = step; + } + + @Override + public Control getControl() { + //TODO: Don't hardcode CLOUD_SCALE check. + return new SliderControl(this, min, max, step, setting != Settings.CLOUD_SCALE ? ControlValueFormatter.number() : NotFineControlValueFormatter.multiplied(0.25f)); + } + + @Override + public void deserialize(String fragment) { + int deserialized = Integer.parseInt(fragment); + deserialized = MathHelper.clamp_int(deserialized, min, max); + if(step > 1) { + deserialized = step * Math.round((float)deserialized / (float)step); + } + store = deserialized; + value = store; + modifiedValue = store; + } + } + + public static class NotFineOptionTickBox extends NotFineOption { + + protected NotFineOptionTickBox(boolean base, OptionImpact impact, OptionFlag... optionFlags) { + super(base, impact, optionFlags); + } + + @Override + public Control getControl() { + return new TickBoxControl(this); + } + + @Override + public void deserialize(String fragment) { + store = Boolean.parseBoolean(fragment); + value = store; + modifiedValue = store; + } + + } + + + public static abstract class NotFineOption implements Option { + private static final NotFineMinecraftOptionsStorage optionStorage = new NotFineMinecraftOptionsStorage(); + private final OptionImpact impact; + private final EnumSet optionFlags = EnumSet.noneOf(OptionFlag.class); + protected final T base; + + protected T value, modifiedValue, store; + protected Settings setting; + + protected NotFineOption(T base, OptionImpact impact, OptionFlag... optionFlags) { + value = base; + modifiedValue = base; + store = base; + this.base = base; + this.impact = impact; + Collections.addAll(this.optionFlags, optionFlags); + } + + public abstract void deserialize(String fragment); + + public T getStore() { + return store; + } + + @Override + public String getName() { + return I18n.format("options." + setting.name().toLowerCase()); + } + + @Override + public String getTooltip() { + return I18n.format("options." + setting.name().toLowerCase() + ".tooltip"); + } + + @Override + public OptionImpact getImpact() { + return impact; + } + + @Override + public T getValue() { + return modifiedValue; + } + + @Override + public void setValue(T value) { + modifiedValue = value; + } + + @Override + public void reset() { + value = store; + modifiedValue = store; + } + + @Override + public OptionStorage getStorage() { + return optionStorage; + } + + @Override + public boolean isAvailable() { + return AngelicaConfig.enableNotFineFeatures; + } + + @Override + public boolean hasChanged() { + return !this.value.equals(this.modifiedValue); + } + + @Override + public void applyChanges() { + store = modifiedValue; + value = modifiedValue; + setting.applyChanges(); + } + + @Override + public Collection getFlags() { + return optionFlags; + } + + } + +} + + diff --git a/src/main/java/jss/notfine/core/SettingsManager.java b/src/main/java/jss/notfine/core/SettingsManager.java new file mode 100644 index 000000000..e63bd6cec --- /dev/null +++ b/src/main/java/jss/notfine/core/SettingsManager.java @@ -0,0 +1,134 @@ +package jss.notfine.core; + +import jss.notfine.config.VideoSettings; +import jss.notfine.gui.options.named.AlwaysNever; +import jss.notfine.gui.options.named.BackgroundSelect; +import jss.notfine.gui.options.named.DownfallQuality; +import jss.notfine.gui.options.named.GraphicsQualityOff; +import jss.notfine.gui.options.named.GraphicsToggle; +import jss.notfine.gui.options.named.LeavesQuality; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.init.Blocks; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.util.ResourceLocation; + +import java.io.File; + +public class SettingsManager { + + private static final Minecraft mc = Minecraft.getMinecraft(); + + public static VideoSettings settingsFile = new VideoSettings( + new File(Launch.minecraftHome + File.separator + "optionsnf.txt") + ); + + public static int minimumFarPlaneDistance; + public static double cloudTranslucencyCheck; + public static boolean shadows; + public static boolean droppedItemDetail; + public static boolean leavesOpaque; + public static boolean waterDetail; + public static boolean vignette; + public static byte downfallDistance; + + //TODO: Hook up using sodium system + public static double entityRenderScaleFactor = 20000; + + public static ResourceLocation defaultBackground = Gui.optionsBackground; + public static ResourceLocation[] extraBackgrounds = new ResourceLocation[] { + new ResourceLocation("textures/blocks/sand.png"), + new ResourceLocation("textures/blocks/mycelium_top.png"), + new ResourceLocation("textures/blocks/stonebrick.png"), + new ResourceLocation("textures/blocks/stonebrick_mossy.png"), + new ResourceLocation("textures/blocks/planks_oak.png"), + new ResourceLocation("textures/blocks/planks_birch.png") + }; + + public static void backgroundUpdated() { + int value = ((BackgroundSelect)Settings.GUI_BACKGROUND.option.getStore()).ordinal(); + if(value == 0) { + Gui.optionsBackground = defaultBackground; + } else { + Gui.optionsBackground = extraBackgrounds[((BackgroundSelect)Settings.GUI_BACKGROUND.option.getStore()).ordinal() - 1]; + } + } + + public static void cloudsUpdated() { + if(Settings.MODE_CLOUDS.option.getStore() != GraphicsQualityOff.OFF) { + minimumFarPlaneDistance = 32 * (int)Settings.RENDER_DISTANCE_CLOUDS.option.getStore(); + minimumFarPlaneDistance += Math.abs((int)Settings.CLOUD_HEIGHT.option.getStore()); + mc.gameSettings.clouds = true; + } else { + minimumFarPlaneDistance = 128; + mc.gameSettings.clouds = false; + } + switch((AlwaysNever)Settings.MODE_CLOUD_TRANSLUCENCY.option.getStore()) { + case DEFAULT -> cloudTranslucencyCheck = (int)Settings.CLOUD_HEIGHT.option.getStore(); + case ALWAYS -> cloudTranslucencyCheck = Double.NEGATIVE_INFINITY; + case NEVER -> cloudTranslucencyCheck = Double.POSITIVE_INFINITY; + } + } + + public static void downfallDistanceUpdated() { + switch((DownfallQuality)Settings.DOWNFALL_DISTANCE.option.getStore()) { + case DEFAULT -> downfallDistance = (byte) (mc.gameSettings.fancyGraphics ? 10 : 5); + case FAST -> downfallDistance = (byte) 5; + case FANCY -> downfallDistance = (byte) 10; + case ULTRA -> downfallDistance = (byte) 15; + case OFF -> downfallDistance = (byte) 0; + } + } + + public static void leavesUpdated() { + //Do not re-enable, see MixinBlockLeaves workaround for Angelica/Sodium style menus. + //mc.renderGlobal.loadRenderers(); + LeavesQuality value = (LeavesQuality)Settings.MODE_LEAVES.option.getStore(); + leavesOpaque = value == LeavesQuality.FANCY || (value == LeavesQuality.DEFAULT && !mc.gameSettings.fancyGraphics); + Blocks.leaves.setGraphicsLevel(!leavesOpaque); + Blocks.leaves2.setGraphicsLevel(!leavesOpaque); + } + + public static void shadowsUpdated() { + switch((GraphicsToggle)Settings.MODE_SHADOWS.option.getStore()) { + case DEFAULT -> shadows = mc.gameSettings.fancyGraphics; + case ON-> shadows = true; + case OFF -> shadows = false; + } + } + + public static void droppedItemDetailUpdated() { + switch((GraphicsQuality)Settings.MODE_DROPPED_ITEMS.option.getStore()) { + case DEFAULT -> droppedItemDetail = mc.gameSettings.fancyGraphics; + case FANCY -> droppedItemDetail = true; + case FAST -> droppedItemDetail = false; + } + } + + public static void waterDetailUpdated() { + switch((GraphicsQuality)Settings.MODE_WATER.option.getStore()) { + case DEFAULT -> waterDetail = mc.gameSettings.fancyGraphics; + case FANCY -> waterDetail = true; + case FAST -> waterDetail = false; + } + } + + public static void vignetteUpdated() { + switch((GraphicsToggle)Settings.MODE_VIGNETTE.option.getStore()) { + case DEFAULT -> vignette = mc.gameSettings.fancyGraphics; + case ON -> vignette = true; + case OFF -> vignette = false; + } + } + + public static void graphicsUpdated() { + downfallDistanceUpdated(); + leavesUpdated(); + shadowsUpdated(); + droppedItemDetailUpdated(); + waterDetailUpdated(); + vignetteUpdated(); + } + +} diff --git a/src/main/java/jss/notfine/gui/GuiCustomMenu.java b/src/main/java/jss/notfine/gui/GuiCustomMenu.java new file mode 100644 index 000000000..494efa4e5 --- /dev/null +++ b/src/main/java/jss/notfine/gui/GuiCustomMenu.java @@ -0,0 +1,103 @@ +package jss.notfine.gui; + +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiListExtended; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.resources.I18n; +import org.lwjgl.input.Keyboard; + +import java.util.HashSet; + +public class GuiCustomMenu extends GuiScreen { + public static GuiCustomMenu instance; + public static HashSet> dirtyStorages = new HashSet<>(); + private final GuiScreen parentGuiScreen; + private final OptionPage optionPage; + private final OptionPage[] subPages; + protected String screenTitle; + + + private GuiListExtended optionsRowList; + + public GuiCustomMenu(GuiScreen parentGuiScreen, OptionPage optionPage, OptionPage... subPages) { + this.parentGuiScreen = parentGuiScreen; + this.screenTitle = optionPage.getName(); + this.optionPage = optionPage; + this.subPages = subPages; + + } + + @Override + public void initGui() { + buttonList.clear(); + buttonList.add(new GuiButton(200, width / 2 - 110, height - 27, I18n.format("gui.done"))); + optionsRowList = new GuiCustomOptionsRowList(mc, width, height, 32, height - 32, 25, optionPage, subPages); + instance = this; + } + + @Override + protected void actionPerformed(GuiButton button) { + if(button.enabled && button.id == 200) { + if(!(parentGuiScreen instanceof GuiCustomMenu)) { + saveChanges(); + } + mc.displayGuiScreen(parentGuiScreen); + } + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { + int originalScale = mc.gameSettings.guiScale; + + super.mouseClicked(mouseX, mouseY, mouseButton); + optionsRowList.func_148179_a(mouseX, mouseY, mouseButton); + + if(mc.gameSettings.guiScale != originalScale) { + ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + setWorldAndResolution(mc, scaledresolution.getScaledWidth(), scaledresolution.getScaledHeight()); + } + } + + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + int originalScale = mc.gameSettings.guiScale; + + super.mouseMovedOrUp(mouseX, mouseY, state); + optionsRowList.func_148181_b(mouseX, mouseY, state); + + if(mc.gameSettings.guiScale != originalScale) { + ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + setWorldAndResolution(mc, scaledresolution.getScaledWidth(), scaledresolution.getScaledHeight()); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + if(mc.theWorld != null && Keyboard.isKeyDown(Keyboard.KEY_F1)) { + return; + } + drawDefaultBackground(); + optionsRowList.drawScreen(mouseX, mouseY, partialTicks); + drawCenteredString(fontRendererObj, screenTitle, width / 2, 5, 16777215); + super.drawScreen(mouseX, mouseY, partialTicks); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) { + if(keyCode == Keyboard.KEY_ESCAPE && !(parentGuiScreen instanceof GuiCustomMenu)) { + saveChanges(); + } + super.keyTyped(typedChar, keyCode); + } + + private void saveChanges() { + for(OptionStorage storage : dirtyStorages) { + storage.save(); + } + dirtyStorages = new HashSet<>(); + } + +} diff --git a/src/main/java/jss/notfine/gui/GuiCustomMenuButton.java b/src/main/java/jss/notfine/gui/GuiCustomMenuButton.java new file mode 100644 index 000000000..b793e75bb --- /dev/null +++ b/src/main/java/jss/notfine/gui/GuiCustomMenuButton.java @@ -0,0 +1,31 @@ +package jss.notfine.gui; + +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; + +public class GuiCustomMenuButton extends GuiButton { + + private final OptionPage optionPage; + private final OptionPage[] subPages; + + public GuiCustomMenuButton(int xPosition, int yPosition, int width, int height, OptionPage optionPage, OptionPage... subPages) { + super(-10, xPosition, yPosition, width, height, optionPage.getName()); + this.optionPage = optionPage; + this.subPages = subPages; + } + + public GuiCustomMenuButton(int xPosition, int yPosition, OptionPage optionPage, OptionPage... subPages) { + this(xPosition, yPosition, 150, 20, optionPage, subPages); + } + + @Override + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + boolean load = super.mousePressed(mc, mouseX, mouseY); + if(load) { + mc.displayGuiScreen(new GuiCustomMenu(mc.currentScreen, optionPage, subPages)); + } + return load; + } + +} diff --git a/src/main/java/jss/notfine/gui/GuiCustomOptionsRowList.java b/src/main/java/jss/notfine/gui/GuiCustomOptionsRowList.java new file mode 100644 index 000000000..f0a5373ef --- /dev/null +++ b/src/main/java/jss/notfine/gui/GuiCustomOptionsRowList.java @@ -0,0 +1,116 @@ +package jss.notfine.gui; + +import com.google.common.collect.Lists; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import jss.notfine.gui.options.control.element.NotFineControlElementFactory; +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiListExtended; +import net.minecraft.client.renderer.Tessellator; + +import java.util.Iterator; +import java.util.List; + +@SideOnly(Side.CLIENT) +public class GuiCustomOptionsRowList extends GuiListExtended { + private static final NotFineControlElementFactory factory = new NotFineControlElementFactory(); + private final List settingsList = Lists.newArrayList(); + + public GuiCustomOptionsRowList(Minecraft mc, int width, int height, int top, int bottom, int slotHeight, OptionPage optionPage, OptionPage... subPages) { + super(mc, width, height, top, bottom, slotHeight); + field_148163_i = false; + + for(OptionGroup optionGroup : optionPage.getGroups()) { + Iterator settings = optionGroup.getOptions().stream().iterator(); + while(settings.hasNext()) { + Option optionOne = (Option)settings.next(); + Option optionTwo = settings.hasNext() ? (Option)settings.next() : null; + GuiButton buttonOne = (GuiButton)optionOne.getControl().createElement(new Dim2i(width / 2 - 155,0,150,20), factory); + GuiButton buttonTwo = optionTwo == null ? null : (GuiButton)optionTwo.getControl().createElement(new Dim2i(width / 2 - 155 + 160,0,150,20), factory); + settingsList.add(new GuiCustomOptionsRowList.Row(buttonOne, buttonTwo)); + } + } + for(int i = 0; i < subPages.length; i += 2) { + OptionPage pageOne = subPages[i]; + OptionPage pageTwo = i < subPages.length - 1 ? subPages[i + 1] : null; + GuiButton buttonOne = new GuiCustomMenuButton(width / 2 - 155, 0, pageOne); + GuiButton buttonTwo = pageTwo == null ? null : new GuiCustomMenuButton(width / 2 - 155 + 160, 0, pageTwo); + settingsList.add(new Row(buttonOne, buttonTwo)); + } + } + + @Override + public GuiCustomOptionsRowList.Row getListEntry(int index) { + return settingsList.get(index); + } + + @Override + protected int getSize() { + return settingsList.size(); + } + + @Override + public int getListWidth() { + return 400; + } + + @Override + protected int getScrollBarX() { + return super.getScrollBarX() + 32; + } + + @SideOnly(Side.CLIENT) + public static class Row implements GuiListExtended.IGuiListEntry { + private final Minecraft mc = Minecraft.getMinecraft(); + private final GuiButton buttonOne, buttonTwo; + + public Row(GuiButton one, GuiButton two) { + buttonOne = one; + buttonTwo = two; + + if(one != null && two == null) { + one.width += 160; + } + } + + @Override + public void drawEntry(int varU1, int x, int y, int varU2, int varU3, Tessellator tessellator, int mouseX, int mouseY, boolean varU4) { + buttonOne.yPosition = y; + buttonOne.drawButton(mc, mouseX, mouseY); + + + if(buttonTwo != null) { + buttonTwo.yPosition = y; + buttonTwo.drawButton(mc, mouseX, mouseY); + } + } + + @Override + public boolean mousePressed(int index, int x, int y, int mouseEvent, int relativeX, int relativeY) { + if(buttonOne.mousePressed(mc, x, y)) { + return true; + } + if(buttonTwo != null && buttonTwo.mousePressed(mc, x, y)) { + return true; + } + return false; + } + + @Override + public void mouseReleased(int index, int x, int y, int mouseEvent, int relativeX, int relativeY) { + if(buttonOne != null) { + buttonOne.mouseReleased(x, y); + } + + if(buttonTwo != null) { + buttonTwo.mouseReleased(x, y); + } + } + } + +} diff --git a/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java b/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java new file mode 100644 index 000000000..894cbe819 --- /dev/null +++ b/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java @@ -0,0 +1,222 @@ +package jss.notfine.gui; + +import com.google.common.collect.ImmutableList; +import jss.notfine.config.NotFineConfig; +import jss.notfine.core.Settings; +import jss.notfine.gui.options.control.NotFineControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; +import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpact; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpl; +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl; +import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl; +import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsMode; +import me.jellysquid.mods.sodium.client.gui.options.named.LightingQuality; +import me.jellysquid.mods.sodium.client.gui.options.named.ParticleMode; +import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.settings.GameSettings; +import org.lwjgl.opengl.Display; + +import java.util.ArrayList; +import java.util.List; + +public class NotFineGameOptionPages { + private static final MinecraftOptionsStorage vanillaOpts = new MinecraftOptionsStorage(); + + public static OptionPage general() { + List groups = new ArrayList<>(); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(GraphicsMode.class, vanillaOpts) + .setName(I18n.format("options.graphics")) + .setTooltip(I18n.format("sodium.options.graphics_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsMode.class)) + .setBinding( + (opts, value) -> opts.fancyGraphics = value.isFancy(), + opts -> GraphicsMode.fromBoolean(opts.fancyGraphics)) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.renderDistance")) + .setTooltip(I18n.format("sodium.options.view_distance.tooltip")) + .setControl(option -> new SliderControl(option, 2, (int)GameSettings.Options.RENDER_DISTANCE.getValueMax(), 1, ControlValueFormatter.quantity("options.chunks"))) + .setBinding((options, value) -> options.renderDistanceChunks = value, options -> options.renderDistanceChunks) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.vsync")) + .setTooltip(I18n.format("sodium.options.v_sync.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> { + opts.enableVsync = value; + Display.setVSyncEnabled(opts.enableVsync); + }, opts -> opts.enableVsync) + .setImpact(OptionImpact.VARIES) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.fullscreen")) + .setTooltip(I18n.format("sodium.options.fullscreen.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> { + opts.fullScreen = value; + Minecraft client = Minecraft.getMinecraft(); + if (client.isFullScreen() != opts.fullScreen) { + client.toggleFullscreen(); + //The client might not be able to enter full-screen mode + opts.fullScreen = client.isFullScreen(); + } + }, (opts) -> opts.fullScreen) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.framerateLimit")) + .setTooltip(I18n.format("sodium.options.fps_limit.tooltip")) + .setControl(option -> new SliderControl(option, 5, 260, 1, ControlValueFormatter.fpsLimit())) + .setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate) + .build()) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.guiScale")) + .setTooltip(I18n.format("sodium.options.gui_scale.tooltip")) + .setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.guiScale())) + .setBinding((opts, value) -> opts.guiScale = value, opts -> opts.guiScale) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.viewBobbing")) + .setTooltip(I18n.format("sodium.options.view_bobbing.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.viewBobbing = value, opts -> opts.viewBobbing) + .build()) + .add(OptionImpl.createBuilder(LightingQuality.class, vanillaOpts) + .setName(I18n.format("options.ao")) + .setTooltip(I18n.format("sodium.options.smooth_lighting.tooltip")) + .setControl(option -> new CyclingControl<>(option, LightingQuality.class)) + .setBinding((opts, value) -> opts.ambientOcclusion = value.getVanilla(), opts -> LightingQuality.fromOrdinal(opts.ambientOcclusion)) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.gamma")) + .setTooltip(I18n.format("sodium.options.brightness.tooltip")) + .setControl(opt -> new SliderControl(opt, 0, 100, 1, ControlValueFormatter.brightness())) + .setBinding((opts, value) -> opts.gammaSetting = value * 0.01F, (opts) -> (int) (opts.gammaSetting / 0.01F)) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.mipmapLevels")) + .setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip")) + .setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier())) + .setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_ASSET_RELOAD) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.anisotropicFiltering")) + .setTooltip(I18n.format("sodium.options.anisotropic_filtering.tooltip")) + .setControl(option -> new SliderControl(option, 0, 4, 1, NotFineControlValueFormatter.powerOfTwo())) + .setBinding( + (opts, value) -> opts.anisotropicFiltering = value == 0 ? 1 : (int)Math.pow(2, value), + (opts) -> opts.anisotropicFiltering == 1 ? 0 : (int)(Math.log(opts.anisotropicFiltering) / Math.log(2))) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_ASSET_RELOAD) + .build()) + .build()); + return new OptionPage(I18n.format("options.video"), ImmutableList.copyOf(groups)); + } + + + public static OptionPage detail() { + List groups = new ArrayList<>(); + groups.add(OptionGroup.createBuilder() + .add(Settings.MODE_LEAVES.option) + .add(Settings.MODE_WATER.option) + .add(Settings.DOWNFALL_DISTANCE.option) + .add(Settings.MODE_VIGNETTE.option) + .add(Settings.MODE_SHADOWS.option) + .add(Settings.MODE_DROPPED_ITEMS.option) + .add(Settings.MODE_GLINT_WORLD.option) + .add(Settings.MODE_GLINT_INV.option) + .build()); + return new OptionPage(I18n.format("options.button.detail"), ImmutableList.copyOf(groups)); + } + + public static OptionPage atmosphere() { + List groups = new ArrayList<>(); + groups.add(OptionGroup.createBuilder() + .add(Settings.MODE_SKY.option) + .add(Settings.MODE_CLOUDS.option) + .add(Settings.RENDER_DISTANCE_CLOUDS.option) + .add(Settings.CLOUD_HEIGHT.option) + .add(Settings.CLOUD_SCALE.option) + .add(Settings.MODE_CLOUD_TRANSLUCENCY.option) + .add(Settings.TOTAL_STARS.option) + .build()); + return new OptionPage(I18n.format("options.button.sky"), ImmutableList.copyOf(groups)); + } + + public static OptionPage particles() { + List groups = new ArrayList<>(); + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(ParticleMode.class, vanillaOpts) + .setName(I18n.format("options.particles")) + .setTooltip(I18n.format("sodium.options.particle_quality.tooltip")) + .setControl(opt -> new CyclingControl<>(opt, ParticleMode.class)) + .setBinding((opts, value) -> opts.particleSetting = value.ordinal(), (opts) -> ParticleMode.fromOrdinal(opts.particleSetting)) + .setImpact(OptionImpact.LOW) + .build()) + .add(Settings.PARTICLES_VOID.option) + .add(Settings.PARTICLES_ENC_TABLE.option) + .build()); + return new OptionPage(I18n.format("options.button.particle"), ImmutableList.copyOf(groups)); + } + + public static OptionPage other() { + List groups = new ArrayList<>(); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.advancedOpengl")) + .setTooltip(I18n.format("sodium.options.advanced_opengl.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advancedOpengl = value, opts -> opts.advancedOpengl) + .setImpact(OptionImpact.VARIES) + .setEnabled(NotFineConfig.allowAdvancedOpenGL) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.fboEnable")) + .setTooltip(I18n.format("sodium.options.fbo.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.fboEnable = value, opts -> opts.fboEnable) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.anaglyph")) + .setTooltip(I18n.format("sodium.options.anaglyph.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.anaglyph = value, opts -> opts.anaglyph) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.showCape")) + .setTooltip(I18n.format("sodium.options.show_cape.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.showCape = value, opts -> opts.showCape) + .build()) + .add(Settings.MODE_GUI_BACKGROUND.option) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(Settings.GUI_BACKGROUND.option) + .build()); + + return new OptionPage(I18n.format("options.button.other"), ImmutableList.copyOf(groups)); + } + +} diff --git a/src/main/java/jss/notfine/gui/options/control/NotFineControlValueFormatter.java b/src/main/java/jss/notfine/gui/options/control/NotFineControlValueFormatter.java new file mode 100644 index 000000000..0d20b5b8d --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/NotFineControlValueFormatter.java @@ -0,0 +1,15 @@ +package jss.notfine.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import net.minecraft.client.resources.I18n; + +public class NotFineControlValueFormatter { + public static ControlValueFormatter multiplied(float multiplier) { + return (value) -> String.valueOf((value * multiplier)); + } + + public static ControlValueFormatter powerOfTwo() { + return (v) -> (v == 0) ? I18n.format("options.off") : I18n.format((int)Math.pow(2, v) + "x"); + } + +} diff --git a/src/main/java/jss/notfine/gui/options/control/element/CyclingControlElement.java b/src/main/java/jss/notfine/gui/options/control/element/CyclingControlElement.java new file mode 100644 index 000000000..e65465004 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/element/CyclingControlElement.java @@ -0,0 +1,42 @@ +package jss.notfine.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.Minecraft; + +public class CyclingControlElement> extends NotFineControlElement { + private final T[] allowedValues; + private final String[] names; + private int currentIndex = 0; + + public CyclingControlElement(Option option, Dim2i dim, T[] allowedValues, String[] names) { + super(option, dim); + this.allowedValues = allowedValues; + this.names = names; + + for(int i = 0; i < allowedValues.length; ++i) { + if(allowedValues[i] == option.getValue()) { + currentIndex = i; + break; + } + } + } + + public String getLabel() { + Enum value = option.getValue(); + return super.getLabel() + names[value.ordinal()]; + } + + @Override + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + if(super.mousePressed(mc, mouseX, mouseY)) { + currentIndex = (option.getValue().ordinal() + 1) % allowedValues.length; + option.setValue(allowedValues[currentIndex]); + onOptionValueChanged(); + return true; + } else { + return false; + } + } + +} diff --git a/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElement.java b/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElement.java new file mode 100644 index 000000000..87913ab61 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElement.java @@ -0,0 +1,75 @@ +package jss.notfine.gui.options.control.element; + +import jss.notfine.gui.GuiCustomMenu; +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.EnumChatFormatting; + +import java.util.Collection; + +public class NotFineControlElement extends GuiButton implements ControlElement { + protected final Option option; + protected final Dim2i dim; + + public NotFineControlElement(Option option, Dim2i dim) { + super(-5, dim.getOriginX(), dim.getOriginY(), dim.getWidth(), dim.getHeight(), option.getName()); + this.option = option; + this.dim = dim; + enabled = option.isAvailable(); + } + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + displayString = getLabel(); + //TODO: tooltips + //hovered + super.drawButton(mc, mouseX, mouseY); + } + + public String getLabel() { + String name = option.getName(); + String label; + enabled = option.isAvailable(); + if(enabled && option.hasChanged()) { + label = EnumChatFormatting.ITALIC + name; + } else { + label = name; + } + label += ": "; + return label; + } + + protected void onOptionValueChanged() { + option.applyChanges(); + + Collection flags = option.getFlags(); + Minecraft mc = Minecraft.getMinecraft(); + if(flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) { + mc.renderGlobal.loadRenderers(); + } + if(flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) { + mc.getTextureMapBlocks().setMipmapLevels(mc.gameSettings.mipmapLevels); + mc.refreshResources(); + } + GuiCustomMenu.dirtyStorages.add(option.getStorage()); + } + + @Override + public boolean isHovered() { + return false; + } + + @Override + public Option getOption() { + return this.option; + } + + @Override + public Dim2i getDimensions() { + return this.dim; + } +} diff --git a/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElementFactory.java b/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElementFactory.java new file mode 100644 index 000000000..9e460e46d --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/element/NotFineControlElementFactory.java @@ -0,0 +1,26 @@ +package jss.notfine.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public class NotFineControlElementFactory implements ControlElementFactory { + + @Override + public > ControlElement cyclingControlElement(Option option, Dim2i dim, T[] allowedValues, String[] names) { + return new CyclingControlElement(option, dim, allowedValues, names); + } + + @Override + public ControlElement sliderControlElement(Option option, Dim2i dim, int min, int max, int interval, ControlValueFormatter formatter) { + return new SliderControlElement(option, dim, min, max, interval, formatter); + } + + @Override + public ControlElement tickBoxElement(Option option, Dim2i dim) { + return new TickBoxControlElement(option, dim); + } + +} diff --git a/src/main/java/jss/notfine/gui/options/control/element/SliderControlElement.java b/src/main/java/jss/notfine/gui/options/control/element/SliderControlElement.java new file mode 100644 index 000000000..d1f8547be --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/element/SliderControlElement.java @@ -0,0 +1,85 @@ +package jss.notfine.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.Minecraft; +import net.minecraft.util.MathHelper; +import org.lwjgl.opengl.GL11; + +public class SliderControlElement extends NotFineControlElement { + private final ControlValueFormatter formatter; + private final int min, max, interval; + + private float value; + private boolean mousePressed; + + public SliderControlElement(Option option, Dim2i dim, int min, int max, int interval, ControlValueFormatter formatter) { + super(option, dim); + this.min = min; + this.max = max; + this.interval = interval; + this.formatter = formatter; + value = option.getValue(); + //Normalize value + value = MathHelper.clamp_float((value - min) / (max - min), 0f, 1f); + } + + public String getLabel() { + return super.getLabel() + formatter.format(option.getValue()); + } + + @Override + public int getHoverState(boolean mouseOver) { + return 0; + } + + @Override + protected void mouseDragged(Minecraft mc, int mouseX, int mouseY) { + if(visible) { + if(mousePressed) { + updateSlider(mouseX); + } + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + drawTexturedModalRect(xPosition + (int)(value * (float)(width - 8)), yPosition, 0, 66, 4, 20); + drawTexturedModalRect(xPosition + (int)(value * (float)(width - 8)) + 4, yPosition, 196, 66, 4, 20); + } + } + + @Override + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + if(super.mousePressed(mc, mouseX, mouseY)) { + updateSlider(mouseX); + mousePressed = true; + return true; + } else { + return false; + } + } + + @Override + public void mouseReleased(int mouseX, int mouseY) { + mousePressed = false; + } + + private void updateSlider(int mouseX) { + value = (float)(mouseX - (xPosition + 4)) / (float)(width - 8); + //Clamp normalized value + value = MathHelper.clamp_float(value, 0f, 1f); + //Un-normalize value + value = min + (max - min) * MathHelper.clamp_float(value, 0f, 1f); + //Clamp value + value = MathHelper.clamp_float(value, min, max); + //Snap value + if(interval > 0) { + value = interval * (float)Math.round(value / interval); + } + //Commit value + option.setValue((int)value); + //Normalize value + value = MathHelper.clamp_float((value - min) / (max - min), 0f, 1f); + + onOptionValueChanged(); + } + +} diff --git a/src/main/java/jss/notfine/gui/options/control/element/TickBoxControlElement.java b/src/main/java/jss/notfine/gui/options/control/element/TickBoxControlElement.java new file mode 100644 index 000000000..f5647f4a2 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/control/element/TickBoxControlElement.java @@ -0,0 +1,28 @@ +package jss.notfine.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; + +public class TickBoxControlElement extends NotFineControlElement { + public TickBoxControlElement(Option option, Dim2i dim) { + super(option, dim); + } + + public String getLabel() { + return super.getLabel() + I18n.format(option.getValue() ? "options.on" : "options.off"); + } + + @Override + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + if(super.mousePressed(mc, mouseX, mouseY)) { + option.setValue(!option.getValue()); + onOptionValueChanged(); + return true; + } else { + return false; + } + } + +} diff --git a/src/main/java/jss/notfine/gui/options/named/AlwaysNever.java b/src/main/java/jss/notfine/gui/options/named/AlwaysNever.java new file mode 100644 index 000000000..64eb404a4 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/AlwaysNever.java @@ -0,0 +1,21 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum AlwaysNever implements NamedState { + DEFAULT("generator.default"), + ALWAYS("options.stream.chat.enabled.always"), + NEVER("options.stream.chat.enabled.never"); + + private final String name; + + AlwaysNever(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} diff --git a/src/main/java/jss/notfine/gui/options/named/BackgroundSelect.java b/src/main/java/jss/notfine/gui/options/named/BackgroundSelect.java new file mode 100644 index 000000000..783c523fa --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/BackgroundSelect.java @@ -0,0 +1,26 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum BackgroundSelect implements NamedState { + DEFAULT("generator.default"), + SAND("tile.sand.default.name"), + MYCELIUM("tile.mycel.name"), + STONEBRICK("tile.stonebricksmooth.name"), + MOSSY_STONEBRICK("tile.stonebricksmooth.mossy.name"), + OAK_PLANKS("tile.wood.oak.name"), + BIRCH_PLANKS("tile.wood.birch.name"); + + private final String name; + + BackgroundSelect(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} + diff --git a/src/main/java/jss/notfine/gui/options/named/DownfallQuality.java b/src/main/java/jss/notfine/gui/options/named/DownfallQuality.java new file mode 100644 index 000000000..fe78ec064 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/DownfallQuality.java @@ -0,0 +1,23 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum DownfallQuality implements NamedState { + DEFAULT("generator.default"), + FANCY("options.graphics.fancy"), + FAST("options.graphics.fast"), + ULTRA("options.graphics.ultra"), + OFF("options.off"); + + private final String name; + + DownfallQuality(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} diff --git a/src/main/java/jss/notfine/gui/options/named/GraphicsQualityOff.java b/src/main/java/jss/notfine/gui/options/named/GraphicsQualityOff.java new file mode 100644 index 000000000..cf833291b --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/GraphicsQualityOff.java @@ -0,0 +1,22 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum GraphicsQualityOff implements NamedState { + DEFAULT("generator.default"), + FANCY("options.graphics.fancy"), + FAST("options.graphics.fast"), + OFF("options.off"); + + private final String name; + + GraphicsQualityOff(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} diff --git a/src/main/java/jss/notfine/gui/options/named/GraphicsToggle.java b/src/main/java/jss/notfine/gui/options/named/GraphicsToggle.java new file mode 100644 index 000000000..4c8900dc8 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/GraphicsToggle.java @@ -0,0 +1,21 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum GraphicsToggle implements NamedState { + DEFAULT("generator.default"), + ON("options.on"), + OFF("options.off"); + + private final String name; + + GraphicsToggle(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} diff --git a/src/main/java/jss/notfine/gui/options/named/LeavesQuality.java b/src/main/java/jss/notfine/gui/options/named/LeavesQuality.java new file mode 100644 index 000000000..fdbb1da15 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/named/LeavesQuality.java @@ -0,0 +1,24 @@ +package jss.notfine.gui.options.named; + +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; + +public enum LeavesQuality implements NamedState { + DEFAULT("generator.default"), + FANCY("options.graphics.fancy"), + FAST("options.graphics.fast"), + SMART("options.graphics.smart"), + SHELLED_FANCY("options.graphics.shelledfancy"), + SHELLED_FAST("options.graphics.shelledfast"); + + private final String name; + + LeavesQuality(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + +} diff --git a/src/main/java/jss/notfine/gui/options/storage/NotFineMinecraftOptionsStorage.java b/src/main/java/jss/notfine/gui/options/storage/NotFineMinecraftOptionsStorage.java new file mode 100644 index 000000000..f5ae66046 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/storage/NotFineMinecraftOptionsStorage.java @@ -0,0 +1,23 @@ +package jss.notfine.gui.options.storage; + +import jss.notfine.NotFine; +import jss.notfine.core.SettingsManager; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; + +public class NotFineMinecraftOptionsStorage implements OptionStorage { + + private final OptionStorageDummy dummy = new OptionStorageDummy(); + + @Override + public OptionStorageDummy getData() { + return dummy; + } + + @Override + public void save() { + SettingsManager.settingsFile.saveSettings(); + + NotFine.logger.info("Flushed changes to NotFine configuration"); + } + +} diff --git a/src/main/java/jss/notfine/gui/options/storage/OptionStorageDummy.java b/src/main/java/jss/notfine/gui/options/storage/OptionStorageDummy.java new file mode 100644 index 000000000..0868c7613 --- /dev/null +++ b/src/main/java/jss/notfine/gui/options/storage/OptionStorageDummy.java @@ -0,0 +1,4 @@ +package jss.notfine.gui.options.storage; + +public class OptionStorageDummy { +} diff --git a/src/main/java/jss/notfine/proxy/ClientProxy.java b/src/main/java/jss/notfine/proxy/ClientProxy.java new file mode 100644 index 000000000..f724de880 --- /dev/null +++ b/src/main/java/jss/notfine/proxy/ClientProxy.java @@ -0,0 +1,44 @@ +package jss.notfine.proxy; + +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.relauncher.Side; +import jss.notfine.config.NotFineConfig; +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.GameSettings; + +public class ClientProxy extends CommonProxy { + + @Override + public void preInit(FMLPreInitializationEvent event) { + if(event.getSide() == Side.CLIENT) { + GameSettings.Options.FRAMERATE_LIMIT.valueStep = 1f; + } + NotFineConfig config = new NotFineConfig(); + config.loadSettings(); + + if(!NotFineConfig.allowAdvancedOpenGL) { + Minecraft.getMinecraft().gameSettings.advancedOpengl = false; + } + + for(Settings setting : Settings.values()) { + setting.ready(); + } + } + + @Override + public void init(FMLInitializationEvent event) { + // Nothing to do here (yet) + } + + @Override + public void postInit(FMLPostInitializationEvent event) { + if(event.getSide() == Side.CLIENT) { + SettingsManager.settingsFile.loadSettings(); + } + } + +} diff --git a/src/main/java/jss/notfine/proxy/CommonProxy.java b/src/main/java/jss/notfine/proxy/CommonProxy.java new file mode 100644 index 000000000..5c86a5746 --- /dev/null +++ b/src/main/java/jss/notfine/proxy/CommonProxy.java @@ -0,0 +1,16 @@ +package jss.notfine.proxy; + +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; + +public class CommonProxy { + + public void preInit(FMLPreInitializationEvent event) { + + } + + public void init(FMLInitializationEvent event) {} + + public void postInit(FMLPostInitializationEvent event) {} +} diff --git a/src/main/java/jss/notfine/render/RenderStars.java b/src/main/java/jss/notfine/render/RenderStars.java new file mode 100644 index 000000000..fe76673eb --- /dev/null +++ b/src/main/java/jss/notfine/render/RenderStars.java @@ -0,0 +1,72 @@ +package jss.notfine.render; + +import jss.notfine.core.Settings; +import jss.util.RandomXoshiro256StarStar; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +public class RenderStars { + + //private static final ResourceLocation locationStarsPng = new ResourceLocation("textures/colormap/stars.png"); + + public static void reloadStarRenderList(RenderGlobal render) { + GL11.glPushMatrix(); + GL11.glNewList(render.starGLCallList, GL11.GL_COMPILE); + renderStars(); + GL11.glEndList(); + GL11.glPopMatrix(); + } + + public static void renderStars() { + final int totalStars = (int)Settings.TOTAL_STARS.option.getStore(); + if(totalStars <= 0) { + return; + } + final RandomXoshiro256StarStar random = new RandomXoshiro256StarStar(10842L); + final Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + + for(int i = 0; i < totalStars; ++i) { + float starOnUnitSphereX = random.nextFloat() * 2.0F - 1.0F; + float starOnUnitSphereY = random.nextFloat() * 2.0F - 1.0F; + float starOnUnitSphereZ = random.nextFloat() * 2.0F - 1.0F; + double distanceNormalizer = starOnUnitSphereX * starOnUnitSphereX + starOnUnitSphereY * starOnUnitSphereY + starOnUnitSphereZ * starOnUnitSphereZ; + + if(distanceNormalizer < 1.0D && distanceNormalizer > 0.01D) { + distanceNormalizer = 1.0D / Math.sqrt(distanceNormalizer); + starOnUnitSphereX *= distanceNormalizer; + starOnUnitSphereY *= distanceNormalizer; + starOnUnitSphereZ *= distanceNormalizer; + final double starX = starOnUnitSphereX * 100.0D; + final double starY = starOnUnitSphereY * 100.0D; + final double starZ = starOnUnitSphereZ * 100.0D; + final double thetaXZ = Math.atan2(starOnUnitSphereX, starOnUnitSphereZ); + final double thetaXZSin = Math.sin(thetaXZ); + final double thetaXZCos = Math.cos(thetaXZ); + final double starAzimuth = Math.atan2(Math.sqrt(starOnUnitSphereX * starOnUnitSphereX + starOnUnitSphereZ * starOnUnitSphereZ), starOnUnitSphereY); + final double starAzimuthX = Math.sin(starAzimuth); + final double starAzimuthZ = Math.cos(starAzimuth); + + final float starSize = 0.15F + random.nextFloat() * 0.1F; + final double starRotation = random.nextDouble() * Math.PI * 2.0D; + final double starRotationSin = Math.sin(starRotation); + final double starRotationCos = Math.cos(starRotation); + + for(int starCorner = 0; starCorner < 4; ++starCorner) { + final double cornerOffsetU = (double)((starCorner & 2) - 1) * starSize; + final double cornerOffsetV = (double)((starCorner + 1 & 2) - 1) * starSize; + final double cornerVerticalOffset = cornerOffsetU * starRotationCos - cornerOffsetV * starRotationSin; + final double cornerHorizontalOffset = cornerOffsetV * starRotationCos + cornerOffsetU * starRotationSin; + final double cornerY = cornerVerticalOffset * starAzimuthX; + final double offsetAzimuthal = -cornerVerticalOffset * starAzimuthZ; + final double cornerX = offsetAzimuthal * thetaXZSin - cornerHorizontalOffset * thetaXZCos; + final double cornerZ = cornerHorizontalOffset * thetaXZSin + offsetAzimuthal * thetaXZCos; + tessellator.addVertex(starX + cornerX, starY + cornerY, starZ + cornerZ); + } + } + } + tessellator.draw(); + } + +} diff --git a/src/main/java/jss/notfine/util/IFaceObstructionCheckHelper.java b/src/main/java/jss/notfine/util/IFaceObstructionCheckHelper.java new file mode 100644 index 000000000..78b488385 --- /dev/null +++ b/src/main/java/jss/notfine/util/IFaceObstructionCheckHelper.java @@ -0,0 +1,9 @@ +package jss.notfine.util; + +import net.minecraft.world.IBlockAccess; + +public interface IFaceObstructionCheckHelper { + + boolean isFaceNonObstructing(IBlockAccess worldIn, int x, int y, int z, int side, double otherMinX, double otherMinY, double otherMinZ, double otherMaxX, double otherMaxY, double otherMaxZ); + +} diff --git a/src/main/java/jss/notfine/util/ILeafBlock.java b/src/main/java/jss/notfine/util/ILeafBlock.java new file mode 100644 index 000000000..8c88e2c10 --- /dev/null +++ b/src/main/java/jss/notfine/util/ILeafBlock.java @@ -0,0 +1,43 @@ +package jss.notfine.util; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import net.minecraft.block.Block; +import net.minecraft.world.IBlockAccess; + +public interface ILeafBlock extends IFaceObstructionCheckHelper { + + @Override() + default boolean isFaceNonObstructing(IBlockAccess worldIn, int x, int y, int z, int side, double otherMinX, double otherMinY, double otherMinZ, double otherMaxX, double otherMaxY, double otherMaxZ) { + + if(Settings.MODE_LEAVES.option.getStore() == LeavesQuality.SHELLED_FAST) { + Block otherBlock; + otherBlock = worldIn.getBlock(x + 1, y, z); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + otherBlock = worldIn.getBlock(x - 1, y, z); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + otherBlock = worldIn.getBlock(x, y + 1, z); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + otherBlock = worldIn.getBlock(x, y - 1, z); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + otherBlock = worldIn.getBlock(x, y, z + 1); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + otherBlock = worldIn.getBlock(x, y, z - 1); + return !(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()); + } else { + return !SettingsManager.leavesOpaque; + } + } + +} diff --git a/src/main/java/jss/notfine/util/LeafRenderUtil.java b/src/main/java/jss/notfine/util/LeafRenderUtil.java new file mode 100644 index 000000000..849b08427 --- /dev/null +++ b/src/main/java/jss/notfine/util/LeafRenderUtil.java @@ -0,0 +1,130 @@ +package jss.notfine.util; + +import jss.notfine.core.Settings; +import jss.notfine.core.SettingsManager; +import jss.notfine.gui.options.named.LeavesQuality; +import net.minecraft.block.Block; +import net.minecraft.util.Facing; +import net.minecraft.world.IBlockAccess; + +public class LeafRenderUtil { + + public static final int[] relativeADirections = { + 2, 3, 4, 5, 0, 1 + }; + + public static final int[] relativeBDirections = { + 3, 2, 5, 4, 1, 0 + }; + + public static final int[] relativeCDirections = { + 4, 5, 0, 1, 2, 3 + }; + + public static final int[] relativeDDirections = { + 5, 4, 1, 0, 3, 2 + }; + + public static boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) { + Block otherBlock = worldIn.getBlock(x, y, z); + if(otherBlock.isOpaqueCube()) { + return false; + } + if(otherBlock instanceof ILeafBlock) { + switch ((LeavesQuality)Settings.MODE_LEAVES.option.getStore()) { + case FANCY, SMART -> { + return false; + } + case SHELLED_FANCY, SHELLED_FAST -> { + x -= Facing.offsetsXForSide[side]; + y -= Facing.offsetsYForSide[side]; + z -= Facing.offsetsZForSide[side]; + int renderCheck = 0; + otherBlock = worldIn.getBlock(x + 1, y, z); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + otherBlock = worldIn.getBlock(x - 1, y, z); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + otherBlock = worldIn.getBlock(x, y + 1, z); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + otherBlock = worldIn.getBlock(x, y - 1, z); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + otherBlock = worldIn.getBlock(x, y, z + 1); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + otherBlock = worldIn.getBlock(x, y, z - 1); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderCheck++; + } + boolean renderSide = renderCheck == 6; + if (renderSide) { + x += 2 * Facing.offsetsXForSide[side]; + y += 2 * Facing.offsetsYForSide[side]; + z += 2 * Facing.offsetsZForSide[side]; + otherBlock = worldIn.getBlock(x, y, z); + if(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube()) { + renderSide = false; + } + x -= Facing.offsetsXForSide[side]; + y -= Facing.offsetsYForSide[side]; + z -= Facing.offsetsZForSide[side]; + int nextSide = relativeADirections[side]; + otherBlock = worldIn.getBlock( + x + Facing.offsetsXForSide[nextSide], + y + Facing.offsetsYForSide[nextSide], + z + Facing.offsetsZForSide[nextSide] + ); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + nextSide = relativeBDirections[side]; + otherBlock = worldIn.getBlock( + x + Facing.offsetsXForSide[nextSide], + y + Facing.offsetsYForSide[nextSide], + z + Facing.offsetsZForSide[nextSide] + ); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + nextSide = relativeCDirections[side]; + otherBlock = worldIn.getBlock( + x + Facing.offsetsXForSide[nextSide], + y + Facing.offsetsYForSide[nextSide], + z + Facing.offsetsZForSide[nextSide] + ); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + nextSide = relativeDDirections[side]; + otherBlock = worldIn.getBlock( + x + Facing.offsetsXForSide[nextSide], + y + Facing.offsetsYForSide[nextSide], + z + Facing.offsetsZForSide[nextSide] + ); + if(!(otherBlock instanceof ILeafBlock || otherBlock.isOpaqueCube())) { + return true; + } + } + return renderSide; + } + default -> { + return !SettingsManager.leavesOpaque; + } + } + } + //Check for IFaceObstructionCheckHelper + if(otherBlock instanceof IFaceObstructionCheckHelper target) { + return target.isFaceNonObstructing(worldIn, x, y, z, side, 0D, 0D, 0D, 1D, 1D, 1D); + } + return true; + } + +} diff --git a/src/main/java/jss/util/RandomXoshiro256StarStar.java b/src/main/java/jss/util/RandomXoshiro256StarStar.java new file mode 100644 index 000000000..a63fe8f31 --- /dev/null +++ b/src/main/java/jss/util/RandomXoshiro256StarStar.java @@ -0,0 +1,189 @@ +/* + * To the extent possible under law, the author has dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * See + */ + +package jss.util; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Implementation of Random based on the xoshiro256** RNG. No-dependencies + * Java port of the original C code, + * which is public domain. This Java port is similarly dedicated to the public + * domain. + *

+ * Individual instances are not thread-safe. Each thread must have its own + * instance which is not shared. + * + * @see http://xoshiro.di.unimi.it/ + * @author David Blackman and Sebastiano Vigna <vigna@acm.org> (original C code) + * @author Una Thompson <una@unascribed.com> (Java port) + */ +public class RandomXoshiro256StarStar extends Random { + private static final long serialVersionUID = -2837799889588687855L; + + private static final AtomicLong uniq = new AtomicLong(System.nanoTime()); + + private static final long nextUniq() { + return splitmix64_2(uniq.addAndGet(SPLITMIX1_MAGIC)); + } + + public RandomXoshiro256StarStar() { + this(System.nanoTime()^nextUniq()); + } + + public RandomXoshiro256StarStar(long seed) { + super(seed); + // super will call setSeed + } + + public RandomXoshiro256StarStar(long s1, long s2, long s3, long s4) { + setState(s1, s2, s3, s4); + } + + // used to "stretch" seeds into a full 256-bit state; also makes + // it safe to pass in zero as a seed + //// + // what generator is used here is unimportant, as long as it's + // from a different family, but splitmix64 happens to be an + // incredibly simple high-quality generator of a completely + // different family (and is recommended by the xoshiro authors) + + private static final long SPLITMIX1_MAGIC = 0x9E3779B97F4A7C15L; + + private static long splitmix64_1(long x) { + return (x + SPLITMIX1_MAGIC); + } + + private static long splitmix64_2(long z) { + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBL; + return z ^ (z >> 31); + } + + @Override + public void setSeed(long seed) { + // update haveNextNextGaussian flag in super + super.setSeed(seed); + long sms = splitmix64_1(seed); + s0 = splitmix64_2(sms); + sms = splitmix64_1(sms); + s1 = splitmix64_2(sms); + sms = splitmix64_1(sms); + s2 = splitmix64_2(sms); + sms = splitmix64_1(sms); + s3 = splitmix64_2(sms); + } + + public void setState(long s0, long s1, long s2, long s4) { + if (s0 == 0 && s1 == 0 && s2 == 0 && s4 == 0) + throw new IllegalArgumentException("xoshiro256** state cannot be all zeroes"); + this.s0 = s0; + this.s1 = s1; + this.s2 = s2; + this.s3 = s4; + } + + // not called, implemented instead of just throwing for completeness + @Override + protected int next(int bits) { + return (int)(nextLong() & ((1L << bits) - 1)); + } + + @Override + public int nextInt() { + return (int)nextLong(); + } + + @Override + public int nextInt(int bound) { + return (int)nextLong(bound); + } + + public long nextLong(long bound) { + if (bound <= 0) throw new IllegalArgumentException("bound must be positive"); + // clear sign bit for positive-only, modulo to bound + return (nextLong() & Long.MAX_VALUE) % bound; + } + + @Override + public double nextDouble() { + return (nextLong() >>> 11) * 0x1.0P-53; + } + + @Override + public float nextFloat() { + return (nextLong() >>> 40) * 0x1.0P-24f; + } + + @Override + public boolean nextBoolean() { + return (nextLong() & 1) != 0; + } + + @Override + public void nextBytes(byte[] buf) { + nextBytes(buf, 0, buf.length); + } + + public void nextBytes(byte[] buf, int ofs, int len) { + if (ofs < 0) throw new ArrayIndexOutOfBoundsException("Offset "+ofs+" is negative"); + if (ofs >= buf.length) throw new ArrayIndexOutOfBoundsException("Offset "+ofs+" is greater than buffer length"); + if (ofs+len > buf.length) throw new ArrayIndexOutOfBoundsException("Length "+len+" with offset "+ofs+" is past end of buffer"); + int j = 8; + long l = 0; + for (int i = ofs; i < ofs+len; i++) { + if (j >= 8) { + l = nextLong(); + j = 0; + } + buf[i] = (byte)(l&0xFF); + l = l >>> 8L; + j++; + } + } + + /* This is xoshiro256** 1.0, our all-purpose, rock-solid generator. It has + excellent (sub-ns) speed, a state (256 bits) that is large enough for + any parallel application, and it passes all tests we are aware of. + + For generating just floating-point numbers, xoshiro256+ is even faster. + + The state must be seeded so that it is not everywhere zero. If you have + a 64-bit seed, we suggest to seed a splitmix64 generator and use its + output to fill s. */ + + private static long rotl(long x, int k) { + return (x << k) | (x >>> (64 - k)); + } + + + private long s0; + private long s1; + private long s2; + private long s3; + + @Override + public long nextLong() { + long result_starstar = rotl(s1 * 5, 7) * 9; + + long t = s1 << 17; + + s2 ^= s0; + s3 ^= s1; + s1 ^= s2; + s0 ^= s3; + + s2 ^= t; + + s3 = rotl(s3, 45); + + return result_starstar; + } + +} diff --git a/src/main/java/kroppeb/stareval/element/AccessibleExpressionElement.java b/src/main/java/kroppeb/stareval/element/AccessibleExpressionElement.java new file mode 100644 index 000000000..52975f669 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/AccessibleExpressionElement.java @@ -0,0 +1,4 @@ +package kroppeb.stareval.element; + +public interface AccessibleExpressionElement extends ExpressionElement { +} diff --git a/src/main/java/kroppeb/stareval/element/Element.java b/src/main/java/kroppeb/stareval/element/Element.java new file mode 100644 index 000000000..6dafe5c38 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/Element.java @@ -0,0 +1,4 @@ +package kroppeb.stareval.element; + +public interface Element { +} diff --git a/src/main/java/kroppeb/stareval/element/ExpressionElement.java b/src/main/java/kroppeb/stareval/element/ExpressionElement.java new file mode 100644 index 000000000..6fa53b100 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/ExpressionElement.java @@ -0,0 +1,4 @@ +package kroppeb.stareval.element; + +public interface ExpressionElement extends Element { +} diff --git a/src/main/java/kroppeb/stareval/element/PriorityOperatorElement.java b/src/main/java/kroppeb/stareval/element/PriorityOperatorElement.java new file mode 100644 index 000000000..e2f323d9f --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/PriorityOperatorElement.java @@ -0,0 +1,7 @@ +package kroppeb.stareval.element; + +public interface PriorityOperatorElement extends Element { + int getPriority(); + + ExpressionElement resolveWith(ExpressionElement right); +} diff --git a/src/main/java/kroppeb/stareval/element/token/BinaryOperatorToken.java b/src/main/java/kroppeb/stareval/element/token/BinaryOperatorToken.java new file mode 100644 index 000000000..1566a4f28 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/token/BinaryOperatorToken.java @@ -0,0 +1,16 @@ +package kroppeb.stareval.element.token; + +import kroppeb.stareval.parser.BinaryOp; + +public class BinaryOperatorToken extends Token { + public final BinaryOp op; + + public BinaryOperatorToken(BinaryOp op) { + this.op = op; + } + + @Override + public String toString() { + return "BinaryOp{" + this.op + "}"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/token/IdToken.java b/src/main/java/kroppeb/stareval/element/token/IdToken.java new file mode 100644 index 000000000..b85061365 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/token/IdToken.java @@ -0,0 +1,20 @@ +package kroppeb.stareval.element.token; + +import kroppeb.stareval.element.AccessibleExpressionElement; + +public class IdToken extends Token implements AccessibleExpressionElement { + private final String id; + + public IdToken(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public String toString() { + return "Id{" + this.id + "}"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/token/NumberToken.java b/src/main/java/kroppeb/stareval/element/token/NumberToken.java new file mode 100644 index 000000000..1f6cfbef4 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/token/NumberToken.java @@ -0,0 +1,20 @@ +package kroppeb.stareval.element.token; + +import kroppeb.stareval.element.ExpressionElement; + +public class NumberToken extends Token implements ExpressionElement { + private final String number; + + public NumberToken(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + @Override + public String toString() { + return "Number{" + this.number + "}"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/token/Token.java b/src/main/java/kroppeb/stareval/element/token/Token.java new file mode 100644 index 000000000..b302aa73c --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/token/Token.java @@ -0,0 +1,8 @@ +package kroppeb.stareval.element.token; + +import kroppeb.stareval.element.Element; + +public abstract class Token implements Element { + @Override + public abstract String toString(); +} diff --git a/src/main/java/kroppeb/stareval/element/token/UnaryOperatorToken.java b/src/main/java/kroppeb/stareval/element/token/UnaryOperatorToken.java new file mode 100644 index 000000000..da3e84aee --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/token/UnaryOperatorToken.java @@ -0,0 +1,29 @@ +package kroppeb.stareval.element.token; + +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.element.PriorityOperatorElement; +import kroppeb.stareval.element.tree.UnaryExpressionElement; +import kroppeb.stareval.parser.UnaryOp; + +public class UnaryOperatorToken extends Token implements PriorityOperatorElement { + private final UnaryOp op; + + public UnaryOperatorToken(UnaryOp op) { + this.op = op; + } + + @Override + public String toString() { + return "UnaryOp{" + this.op + "}"; + } + + @Override + public int getPriority() { + return -1; + } + + @Override + public UnaryExpressionElement resolveWith(ExpressionElement right) { + return new UnaryExpressionElement(this.op, right); + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/AccessExpressionElement.java b/src/main/java/kroppeb/stareval/element/tree/AccessExpressionElement.java new file mode 100644 index 000000000..033303a2a --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/AccessExpressionElement.java @@ -0,0 +1,26 @@ +package kroppeb.stareval.element.tree; + +import kroppeb.stareval.element.AccessibleExpressionElement; + +public class AccessExpressionElement implements AccessibleExpressionElement { + private final AccessibleExpressionElement base; + private final String index; + + public AccessExpressionElement(AccessibleExpressionElement base, String index) { + this.base = base; + this.index = index; + } + + public AccessibleExpressionElement getBase() { + return this.base; + } + + public String getIndex() { + return this.index; + } + + @Override + public String toString() { + return "Access{" + this.base + "[" + this.index + "]}"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/BinaryExpressionElement.java b/src/main/java/kroppeb/stareval/element/tree/BinaryExpressionElement.java new file mode 100644 index 000000000..ded159e6f --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/BinaryExpressionElement.java @@ -0,0 +1,33 @@ +package kroppeb.stareval.element.tree; + +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.parser.BinaryOp; + +public class BinaryExpressionElement implements ExpressionElement { + private final BinaryOp op; + private final ExpressionElement left; + private final ExpressionElement right; + + public BinaryExpressionElement(BinaryOp op, ExpressionElement left, ExpressionElement right) { + this.op = op; + this.left = left; + this.right = right; + } + + public BinaryOp getOp() { + return this.op; + } + + public ExpressionElement getLeft() { + return this.left; + } + + public ExpressionElement getRight() { + return this.right; + } + + @Override + public String toString() { + return "BinaryExpr{ {" + this.left + "} " + this.op + " {" + this.right + "} }"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/FunctionCall.java b/src/main/java/kroppeb/stareval/element/tree/FunctionCall.java new file mode 100644 index 000000000..12bb8ab07 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/FunctionCall.java @@ -0,0 +1,28 @@ +package kroppeb.stareval.element.tree; + +import kroppeb.stareval.element.ExpressionElement; + +import java.util.List; + +public class FunctionCall implements ExpressionElement { + private final String id; + private final List args; + + public FunctionCall(String id, List args) { + this.id = id; + this.args = args; + } + + public String getId() { + return this.id; + } + + public List getArgs() { + return this.args; + } + + @Override + public String toString() { + return "FunctionCall{" + this.id + " {" + this.args + "} }"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/UnaryExpressionElement.java b/src/main/java/kroppeb/stareval/element/tree/UnaryExpressionElement.java new file mode 100644 index 000000000..4126b549a --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/UnaryExpressionElement.java @@ -0,0 +1,27 @@ +package kroppeb.stareval.element.tree; + +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.parser.UnaryOp; + +public class UnaryExpressionElement implements ExpressionElement { + private final UnaryOp op; + private final ExpressionElement inner; + + public UnaryExpressionElement(UnaryOp op, ExpressionElement inner) { + this.op = op; + this.inner = inner; + } + + public UnaryOp getOp() { + return op; + } + + public ExpressionElement getInner() { + return inner; + } + + @Override + public String toString() { + return "UnaryExpr{" + this.op + " {" + this.inner + "} }"; + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/partial/PartialBinaryExpression.java b/src/main/java/kroppeb/stareval/element/tree/partial/PartialBinaryExpression.java new file mode 100644 index 000000000..ae6041c37 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/partial/PartialBinaryExpression.java @@ -0,0 +1,31 @@ +package kroppeb.stareval.element.tree.partial; + +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.element.PriorityOperatorElement; +import kroppeb.stareval.element.tree.BinaryExpressionElement; +import kroppeb.stareval.parser.BinaryOp; + +public class PartialBinaryExpression extends PartialExpression implements PriorityOperatorElement { + private final ExpressionElement left; + private final BinaryOp op; + + public PartialBinaryExpression(ExpressionElement left, BinaryOp op) { + this.left = left; + this.op = op; + } + + @Override + public String toString() { + return "PartialBinaryExpression{ {" + this.left + "} " + this.op + "}"; + } + + @Override + public int getPriority() { + return this.op.getPriority(); + } + + @Override + public BinaryExpressionElement resolveWith(ExpressionElement right) { + return new BinaryExpressionElement(this.op, this.left, right); + } +} diff --git a/src/main/java/kroppeb/stareval/element/tree/partial/PartialExpression.java b/src/main/java/kroppeb/stareval/element/tree/partial/PartialExpression.java new file mode 100644 index 000000000..83512faad --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/partial/PartialExpression.java @@ -0,0 +1,8 @@ +package kroppeb.stareval.element.tree.partial; + +import kroppeb.stareval.element.Element; + +public abstract class PartialExpression implements Element { + @Override + public abstract String toString(); +} diff --git a/src/main/java/kroppeb/stareval/element/tree/partial/UnfinishedArgsExpression.java b/src/main/java/kroppeb/stareval/element/tree/partial/UnfinishedArgsExpression.java new file mode 100644 index 000000000..b0b5d3691 --- /dev/null +++ b/src/main/java/kroppeb/stareval/element/tree/partial/UnfinishedArgsExpression.java @@ -0,0 +1,15 @@ +package kroppeb.stareval.element.tree.partial; + +import kroppeb.stareval.element.ExpressionElement; + +import java.util.ArrayList; +import java.util.List; + +public class UnfinishedArgsExpression extends PartialExpression { + public final List tokens = new ArrayList<>(); + + @Override + public String toString() { + return "UnfinishedArgs{" + this.tokens + "}"; + } +} diff --git a/src/main/java/kroppeb/stareval/exception/MissingTokenException.java b/src/main/java/kroppeb/stareval/exception/MissingTokenException.java new file mode 100644 index 000000000..ade5fbf04 --- /dev/null +++ b/src/main/java/kroppeb/stareval/exception/MissingTokenException.java @@ -0,0 +1,7 @@ +package kroppeb.stareval.exception; + +public class MissingTokenException extends ParseException { + public MissingTokenException(String message, int index) { + super(message + " at index " + index); + } +} diff --git a/src/main/java/kroppeb/stareval/exception/ParseException.java b/src/main/java/kroppeb/stareval/exception/ParseException.java new file mode 100644 index 000000000..3a4f56106 --- /dev/null +++ b/src/main/java/kroppeb/stareval/exception/ParseException.java @@ -0,0 +1,22 @@ +package kroppeb.stareval.exception; + +public abstract class ParseException extends Exception{ + public ParseException() { + } + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } + + public ParseException(Throwable cause) { + super(cause); + } + + public ParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/kroppeb/stareval/exception/UnexpectedCharacterException.java b/src/main/java/kroppeb/stareval/exception/UnexpectedCharacterException.java new file mode 100644 index 000000000..9de12af20 --- /dev/null +++ b/src/main/java/kroppeb/stareval/exception/UnexpectedCharacterException.java @@ -0,0 +1,19 @@ +package kroppeb.stareval.exception; + +public class UnexpectedCharacterException extends ParseException { + public UnexpectedCharacterException(char expected, char actual, int index) { + this("Expected to read '" + expected + "' but got '" + actual + "' at index " + index); + } + + public UnexpectedCharacterException(char actual, int index) { + this("Read an unexpected character '" + actual + "' at index " + index); + } + + public UnexpectedCharacterException(String expected, char actual, int index) { + this("Expected to read " + expected + " but got '" + actual + "' at index " + index); + } + + private UnexpectedCharacterException(String message) { + super(message); + } +} diff --git a/src/main/java/kroppeb/stareval/exception/UnexpectedEndingException.java b/src/main/java/kroppeb/stareval/exception/UnexpectedEndingException.java new file mode 100644 index 000000000..1b2135bbd --- /dev/null +++ b/src/main/java/kroppeb/stareval/exception/UnexpectedEndingException.java @@ -0,0 +1,11 @@ +package kroppeb.stareval.exception; + +public class UnexpectedEndingException extends ParseException { + public UnexpectedEndingException() { + this("Expected to read more text, but the string has ended"); + } + + public UnexpectedEndingException(String message) { + super(message); + } +} diff --git a/src/main/java/kroppeb/stareval/exception/UnexpectedTokenException.java b/src/main/java/kroppeb/stareval/exception/UnexpectedTokenException.java new file mode 100644 index 000000000..d395cc498 --- /dev/null +++ b/src/main/java/kroppeb/stareval/exception/UnexpectedTokenException.java @@ -0,0 +1,7 @@ +package kroppeb.stareval.exception; + +public class UnexpectedTokenException extends ParseException { + public UnexpectedTokenException(String message, int index) { + super(message + " at index " + index); + } +} diff --git a/src/main/java/kroppeb/stareval/parser/BinaryOp.java b/src/main/java/kroppeb/stareval/parser/BinaryOp.java new file mode 100644 index 000000000..41bf081a7 --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/BinaryOp.java @@ -0,0 +1,20 @@ +package kroppeb.stareval.parser; + +public class BinaryOp { + private final String name; + private final int priority; + + public BinaryOp(String name, int priority) { + this.name = name; + this.priority = priority; + } + + @Override + public String toString() { + return this.name + "{" + this.priority + "}"; + } + + public int getPriority() { + return this.priority; + } +} diff --git a/src/main/java/kroppeb/stareval/parser/OpResolver.java b/src/main/java/kroppeb/stareval/parser/OpResolver.java new file mode 100644 index 000000000..e9ada48bd --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/OpResolver.java @@ -0,0 +1,157 @@ +package kroppeb.stareval.parser; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import kroppeb.stareval.exception.ParseException; + +import java.util.Map; + +/** + * OpResolver maps the trailing characters identifying an operator to an actual operator. + * @param + */ +abstract class OpResolver { + abstract T resolve(StringReader input) throws ParseException; + + static class Builder { + private final Map map; + + public Builder() { + map = new Object2ObjectOpenHashMap<>(); + } + + public void singleChar(T op) { + multiChar("", op); + } + + /** + * Adds a new multi-character operator. Calling this with an empty string is equivalent to calling singleChar. + * + * @param trailing every character after the first character of the operator. + * @param op the operator + */ + public void multiChar(String trailing, T op) { + T previous = map.put(trailing, op); + + if (previous != null) { + throw new RuntimeException("Tried to add multiple operators that map to the same string."); + } + } + + public OpResolver build() { + if (map.size() > 2) { + throw new RuntimeException("unimplemented: Cannot currently build an optimized operator resolver " + + "tree when more than two operators start with the same character"); + } + + T singleChar = map.get(""); + + if (singleChar != null) { + if (map.size() == 1) { + return new SingleChar<>(singleChar); + } else { + for (Map.Entry subEntry : map.entrySet()) { + if ("".equals(subEntry.getKey())) { + // We already checked this key + continue; + } + + if (subEntry.getKey().length() != 1) { + throw new RuntimeException("unimplemented: Optimized operator resolver trees can " + + "currently only be built of operators that contain one or two characters."); + } + + // We can assume that this is the only other entry in the map due to the size check above + return new SingleDualChar<>( + singleChar, + subEntry.getValue(), + subEntry.getKey().charAt(0) + ); + } + } + } else { + if (map.size() > 1) { + throw new RuntimeException("unimplemented: Optimized operator resolver trees can currently only " + + "handle two operators starting with the same character if one operator is a single character"); + } + + for (Map.Entry subEntry : map.entrySet()) { + if (subEntry.getKey().length() != 1) { + throw new RuntimeException("unimplemented: Optimized operator resolver trees can " + + "currently only be built of operators that contain one or two characters."); + } + + // We can assume that this is the only entry in the map due to the size check above. + return new DualChar<>( + subEntry.getValue(), + subEntry.getKey().charAt(0) + ); + } + } + + if (map.isEmpty()) { + throw new RuntimeException("Tried to build an operator resolver tree that contains no operators."); + } else { + throw new RuntimeException("This shouldn't be reachable"); + } + } + } + + /** + * Matches a single-character operator. + */ + static class SingleChar extends OpResolver { + private final T op; + + SingleChar(T op) { + this.op = op; + } + + @Override + T resolve(StringReader input) { + return this.op; + } + } + + /** + * Matches a two-character operator. + */ + static class DualChar extends OpResolver { + private final T op; + private final char secondChar; + + DualChar(T op, char secondChar) { + this.op = op; + this.secondChar = secondChar; + } + + @Override + T resolve(StringReader input) throws ParseException { + input.read(this.secondChar); + return this.op; + } + } + + /** + * Matches either a dual character operator or a single character operator. + */ + static class SingleDualChar extends OpResolver { + private final T singleCharOperator; + private final T doubleCharOperator; + private final char secondChar; + + SingleDualChar(T singleCharOperator, T doubleCharOperator, char secondChar) { + this.singleCharOperator = singleCharOperator; + this.doubleCharOperator = doubleCharOperator; + this.secondChar = secondChar; + } + + @Override + T resolve(StringReader input) { + if (input.tryRead(this.secondChar)) { + return this.doubleCharOperator; + } else { + return this.singleCharOperator; + } + } + } +} diff --git a/src/main/java/kroppeb/stareval/parser/Parser.java b/src/main/java/kroppeb/stareval/parser/Parser.java new file mode 100644 index 000000000..4824c6f48 --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/Parser.java @@ -0,0 +1,315 @@ +package kroppeb.stareval.parser; + +import kroppeb.stareval.element.AccessibleExpressionElement; +import kroppeb.stareval.element.Element; +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.element.PriorityOperatorElement; +import kroppeb.stareval.element.token.BinaryOperatorToken; +import kroppeb.stareval.element.token.IdToken; +import kroppeb.stareval.element.token.NumberToken; +import kroppeb.stareval.element.token.UnaryOperatorToken; +import kroppeb.stareval.element.tree.AccessExpressionElement; +import kroppeb.stareval.element.tree.BinaryExpressionElement; +import kroppeb.stareval.element.tree.FunctionCall; +import kroppeb.stareval.element.tree.UnaryExpressionElement; +import kroppeb.stareval.element.tree.partial.PartialBinaryExpression; +import kroppeb.stareval.element.tree.partial.UnfinishedArgsExpression; +import kroppeb.stareval.exception.MissingTokenException; +import kroppeb.stareval.exception.ParseException; +import kroppeb.stareval.exception.UnexpectedTokenException; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * A parser for parsing expressions with operator precedences. + *

+ * I can't actually find a parser type on wikipedia that matches this type of parser + *

+ * The following parser is a bottom-up parser, meaning that the input tokens get combined into elements + * which then get merged with other elements and tokens until a valid expression is formed, or an expression + * is thrown. + *

+ * The uniqueness of this parser lies in how it handles operator precedence without using any lookahead, instead any + * binary operators trigger a simplification on the left-hand side of the operator until the left-hand side has a + * precedence that is strictly higher than the new operator (assuming any expression that is not operator-based is the + * highest possible precedence, eg: function calls, numbers and brackets). Note that making this relationship require + * higher or equal, would make the operator right associative instead of left associative, in case that is ever needed. + *

+ * Once the left-hand side of a binary operator has been sufficiently combined, the operator is combined with the + * expression and is converted into a "partial binary expression" which acts like a unary operator with the precedence + * level. The comments inside {@link #visitBinaryOperator} show a few cases on what this reduction does with a given + * stack. + *

+ * The parsing of brackets is a bit strange, and due to the fact this documentation has been written 5 months after I + * made the design decision and 3 months after my latest code change, I can't fully explain why I put mutable state + * inside one of the elements of the parser, as it does not provide any significant speedup that I could think of. + *

+ * When an opening parenthesis is encountered, an "unfinished argument list" is pushed to the stack. Any comma will + * fully combine the expression on the left of the comma and push it to that list. When a closing parenthesis is + * encountered, a similar reduction is performed if the top of the stack is an expression. Then the parser checks if + * the top of the stack is a Identifier, if so, this is a call expression, otherwise it is simply a bracketed expression + *

+ * + * @author Kroppeb + */ +public class Parser { + private final List stack = new ArrayList<>(); + + Parser() { + } + + private Element peek() { + if (!this.stack.isEmpty()) { + return this.stack.get(this.stack.size() - 1); + } + + return null; + } + + private Element pop() { + if (this.stack.isEmpty()) { + throw new IllegalStateException("Internal token stack is empty"); + } + + return this.stack.remove(this.stack.size() - 1); + } + + private void push(Element element) { + this.stack.add(element); + } + + /** + * @throws ClassCastException if the top is not an expression + * @see #expressionReducePop(int) + */ + private ExpressionElement expressionReducePop() { + return this.expressionReducePop(Integer.MAX_VALUE); + } + + /** + * Pops an expression after trying to reduce the stack. + * Executes following reduce steps: + *
    + *
  • {@link PriorityOperatorElement}, {@link ExpressionElement} => {@link ExpressionElement} + * as long as the {@code priority} of the {@link PriorityOperatorElement} is stricter than the given priority
  • + *
+ * + * @throws ClassCastException if the top is not an expression + */ + private ExpressionElement expressionReducePop(int priority) { + ExpressionElement token = (ExpressionElement) this.pop(); + + while (!this.stack.isEmpty()) { + Element x = this.peek(); + + if (x instanceof PriorityOperatorElement && ((PriorityOperatorElement) x).getPriority() <= priority) { + this.pop(); + token = ((PriorityOperatorElement) x).resolveWith(token); + } else { + break; + } + } + + return token; + } + + /** + * Executes following reduce step: + *
    + *
  • {@link UnfinishedArgsExpression}, {@link #expressionReducePop} => {@link UnfinishedArgsExpression}
  • + *
+ * + * @param index the current reader index, for exception throwing. + * @throws ClassCastException if the top is not an expression + */ + private void commaReduce(int index) throws ParseException { + // ( expr, + // UnfinishedArgs Expression (commaReduce) + // => UnfinishedArgs + + ExpressionElement expr = this.expressionReducePop(); + Element args = this.peek(); + + if (args == null) { + throw new MissingTokenException( + "Expected an opening bracket '(' before seeing a comma ',' or closing bracket ')'", + index + ); + } + + if (args instanceof UnfinishedArgsExpression) { + ((UnfinishedArgsExpression) args).tokens.add(expr); + } else { + throw new UnexpectedTokenException( + "Expected to see an opening bracket '(' or a comma ',' right before an expression followed by a " + + "closing bracket ')' or a comma ','", index); + } + } + + // visitor methods + + void visitId(String id) { + this.push(new IdToken(id)); + } + + boolean canReadAccess() { + return this.peek() instanceof AccessibleExpressionElement; + } + + /** + * Assumes `canReadAccess` has returned true + */ + void visitAccess(String access) { + AccessibleExpressionElement pop = (AccessibleExpressionElement) this.pop(); + this.push(new AccessExpressionElement(pop, access)); + } + + void visitNumber(String numberString) { + this.push(new NumberToken(numberString)); + } + + void visitOpeningParenthesis() { + this.push(new UnfinishedArgsExpression()); + } + + void visitComma(int index) throws ParseException { + if (this.peek() instanceof ExpressionElement) { + this.commaReduce(index); + } else { + throw new UnexpectedTokenException("Expected an expression before a comma ','", index); + } + } + + /** + * Allows for trailing comma. + * Executes following reduce steps: + *
    + *
  • {@link IdToken} {@link UnfinishedArgsExpression}, {@link #expressionReducePop} => {@link FunctionCall}
  • + *
  • {@link IdToken} {@link UnfinishedArgsExpression} => {@link FunctionCall}
  • + *
  • {@link UnfinishedArgsExpression}, {@link #expressionReducePop} => {@link ExpressionElement}
  • + *
+ * + * @param index the current reader index, for exception throwing. + */ + void visitClosingParenthesis(int index) throws ParseException { + // + // ( ... ) + // UnfinishedArgsExpression Expression? (callReduce) + + boolean expressionOnTop = this.peek() instanceof ExpressionElement; + if (expressionOnTop) { + this.commaReduce(index); + } + + UnfinishedArgsExpression args; + { + if (this.stack.isEmpty()) { + throw new MissingTokenException("A closing bracket ')' can't be the first character of an expression", index); + } + + Element pop = this.pop(); + + if (!(pop instanceof UnfinishedArgsExpression)) { + throw new UnexpectedTokenException( + "Expected to see an opening bracket '(' or a comma ',' right before an expression followed by a " + + "closing bracket ')' or a comma ','", index); + } + args = (UnfinishedArgsExpression) pop; + } + + Element top = this.peek(); + + if (top instanceof IdToken) { + this.pop(); + this.push(new FunctionCall(((IdToken) top).getId(), args.tokens)); + } else { + if (args.tokens.isEmpty()) { + throw new MissingTokenException("Encountered empty brackets that aren't a call", index); + } else if (args.tokens.size() > 1) { + throw new UnexpectedTokenException("Encountered too many expressions in brackets that aren't a call", index); + } else if (!expressionOnTop) { + throw new UnexpectedTokenException("Encountered a trailing comma in brackets that aren't a call", index); + } else { + this.push(args.tokens.get(0)); + } + } + } + + boolean canReadBinaryOp() { + return this.peek() instanceof ExpressionElement; + } + + /** + * Executes following reduce steps: + *
    + *
  • {@link ExpressionElement} | {@link BinaryOperatorToken} => {@link PartialBinaryExpression}
  • + *
  • {@link UnaryOperatorToken}, {@link ExpressionElement} | {@link BinaryOperatorToken} => {@link UnaryExpressionElement} | {@link BinaryOperatorToken}
  • + *
  • + * {@link PartialBinaryExpression}, {@link ExpressionElement} | {@link BinaryOperatorToken}
    + * where the operator on the stack has a higher or equal priority to the one being added, the 3 items on the + * stack get popped, merged to a {@link BinaryExpressionElement} and placed on the stack. + * The new token is then pushed again. + *
  • + *
+ */ + void visitBinaryOperator(BinaryOp binaryOp) { + // reduce the expressions to the needed priority level + ExpressionElement left = this.expressionReducePop(binaryOp.getPriority()); + // stack[ {'a', '*'}, 'b'], token = '+' -> stack[], left = {'a', '*', 'b'} + // -> stack[{{'a', '*', 'b'}, '+'}] + // stack[ {'a', '+'}, 'b'], token = '+' -> stack[], left = {'a', '+', 'b'} + // -> stack[{{'a', '+', 'b'}, '+'}] + // stack[ { '-'}, 'b'], token = '+' -> stack[], left = {'-', 'b'} + // -> stack[{{ '-', 'b'}, '+'}] + + // stack[ {'a', '+'}, 'b'], token = '*' -> stack[{'a', '+'}], left = {'b'} + // -> stack[{'a', '+'}, {'b', '*'}] + + this.stack.add(new PartialBinaryExpression(left, binaryOp)); + } + + void visitUnaryOperator(UnaryOp unaryOp) { + this.push(new UnaryOperatorToken(unaryOp)); + } + + ExpressionElement getFinal(int endIndex) throws ParseException { + if (!this.stack.isEmpty()) { + if (this.peek() instanceof ExpressionElement) { + ExpressionElement result = this.expressionReducePop(); + + if (this.stack.isEmpty()) { + return result; + } + + if (this.peek() instanceof UnfinishedArgsExpression) { + throw new MissingTokenException("Expected a closing bracket", endIndex); + } else { + throw new UnexpectedTokenException( + "The stack of tokens isn't empty at the end of the expression: " + this.stack + + " top: " + result, endIndex); + } + } else { + Element top = this.peek(); + if (top instanceof UnfinishedArgsExpression) { + throw new MissingTokenException("Expected a closing bracket", endIndex); + } else if (top instanceof PriorityOperatorElement) { + throw new MissingTokenException( + "Expected a identifier, constant or subexpression on the right side of the operator", + endIndex); + } else { + throw new UnexpectedTokenException( + "The stack of tokens contains an unexpected token at the top: " + this.stack, + endIndex); + } + } + } else { + throw new MissingTokenException("The input seems to be empty", endIndex); + } + } + + public static ExpressionElement parse(String input, ParserOptions options) throws ParseException { + return Tokenizer.parse(input, options); + } +} diff --git a/src/main/java/kroppeb/stareval/parser/ParserOptions.java b/src/main/java/kroppeb/stareval/parser/ParserOptions.java new file mode 100644 index 000000000..2c3ee04ed --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/ParserOptions.java @@ -0,0 +1,119 @@ +package kroppeb.stareval.parser; + +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; + +public final class ParserOptions { + private final Char2ObjectMap> unaryOpResolvers; + private final Char2ObjectMap> binaryOpResolvers; + private final TokenRules tokenRules; + + private ParserOptions( + Char2ObjectMap> unaryOpResolvers, + Char2ObjectMap> binaryOpResolvers, + TokenRules tokenRules) { + this.unaryOpResolvers = unaryOpResolvers; + this.binaryOpResolvers = binaryOpResolvers; + this.tokenRules = tokenRules; + } + + TokenRules getTokenRules() { + return this.tokenRules; + } + + OpResolver getUnaryOpResolver(char c) { + return this.unaryOpResolvers.get(c); + } + + OpResolver getBinaryOpResolver(char c) { + return this.binaryOpResolvers.get(c); + } + + public static class Builder { + private final Char2ObjectMap> unaryOpResolvers = new Char2ObjectOpenHashMap<>(); + private final Char2ObjectMap> binaryOpResolvers = new Char2ObjectOpenHashMap<>(); + private TokenRules tokenRules = TokenRules.DEFAULT; + + public void addUnaryOp(String s, UnaryOp op) { + char first = s.charAt(0); + String trailing = s.substring(1); + + this.unaryOpResolvers.computeIfAbsent(first, (c) -> new OpResolver.Builder<>()).multiChar(trailing, op); + } + + public void addBinaryOp(String s, BinaryOp op) { + char first = s.charAt(0); + String trailing = s.substring(1); + + this.binaryOpResolvers.computeIfAbsent(first, (c) -> new OpResolver.Builder<>()).multiChar(trailing, op); + } + + public void setTokenRules(TokenRules tokenRules) { + this.tokenRules = tokenRules; + } + + private static Char2ObjectMap> buildOpResolvers( + Char2ObjectMap> ops) { + Char2ObjectMap> result = new Char2ObjectOpenHashMap<>(); + + ops.char2ObjectEntrySet().forEach( + entry -> result.put(entry.getCharKey(), entry.getValue().build())); + + return result; + } + + public ParserOptions build() { + return new ParserOptions( + buildOpResolvers(this.unaryOpResolvers), + buildOpResolvers(this.binaryOpResolvers), + this.tokenRules); + } + } + + /** + * Defines a set of rules that allows the tokenizer to identify tokens within a string. + */ + public interface TokenRules { + TokenRules DEFAULT = new TokenRules() {}; + + static boolean isNumber(final char c) { + return c >= '0' && c <= '9'; + } + + static boolean isLowerCaseLetter(final char c) { + return c >= 'a' && c <= 'z'; + } + + static boolean isUpperCaseLetter(final char c) { + return c >= 'A' && c <= 'Z'; + } + + static boolean isLetter(final char c) { + return isLowerCaseLetter(c) || isUpperCaseLetter(c); + } + + default boolean isIdStart(final char c) { + return isLetter(c) || c == '_'; + } + + default boolean isIdPart(final char c) { + return this.isIdStart(c) || isNumber(c); + } + + default boolean isNumberStart(final char c) { + return isNumber(c) || c == '.'; + } + + default boolean isNumberPart(final char c) { + return this.isNumberStart(c) || isLetter(c); + } + + default boolean isAccessStart(final char c) { + return this.isIdStart(c) || isNumber(c); + } + + default boolean isAccessPart(final char c) { + return this.isAccessStart(c); + } + } +} diff --git a/src/main/java/kroppeb/stareval/parser/StringReader.java b/src/main/java/kroppeb/stareval/parser/StringReader.java new file mode 100644 index 000000000..9c2c6a0e2 --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/StringReader.java @@ -0,0 +1,138 @@ +package kroppeb.stareval.parser; + +import kroppeb.stareval.exception.UnexpectedCharacterException; + +/** + * A class to facilitate the reading of strings. + */ +public class StringReader { + /** + * The string we are reading + */ + private final String string; + /** + * The next index to read at + */ + private int nextIndex = -1; + /** + * The last index read + */ + private int lastIndex; + /** + * The start of {@link #substring()} + */ + private int mark; + + + public StringReader(String string) { + this.string = string; + this.advanceOneCharacter(); + this.skipWhitespace(); + + // Initializing these variables to the actual start of the string. + this.lastIndex = this.nextIndex; + this.mark(); + } + + /** + * moves the indexes, nextIndex will skip all spaces. + */ + private void advanceOneCharacter() { + this.lastIndex = this.nextIndex; + + if (this.nextIndex >= this.string.length()) { + return; + } + + this.nextIndex++; + } + + /** + * Skips all whitespace characters until a non-whitespace character is encountered. + */ + public void skipWhitespace() { + while (this.nextIndex < this.string.length() && Character.isWhitespace(this.string.charAt(this.nextIndex))) { + this.nextIndex++; + } + } + + /** + * @return The character that would be returned by the next call to {@link #read} + */ + public char peek() { + return this.string.charAt(this.nextIndex); + } + + /** + * Skips the current character + */ + public void skipOneCharacter() { + this.advanceOneCharacter(); + } + + /** + * Read a character + */ + public char read() { + char current = this.peek(); + this.skipOneCharacter(); + return current; + } + + /** + * Read a character and verify it's the expected character. + */ + public void read(char c) throws UnexpectedCharacterException { + char read = this.read(); + + if (read != c) { + throw new UnexpectedCharacterException(c, read, this.getCurrentIndex()); + } + } + + /** + * Try to read a character. + * + * @param c the character to read + * @return whether it could read the character. + */ + public boolean tryRead(char c) { + if (!this.canRead()) { + return false; + } + + char read = this.peek(); + + if (read != c) { + return false; + } + + this.skipOneCharacter(); + return true; + } + + /** + * Place a mark at the character that just got read. Used for {@link #substring()} + */ + public void mark() { + this.mark = this.lastIndex; + } + + /** + * @return a string, starting at the last call to {@link #mark()}, up to and including the last read character + */ + public String substring() { + return this.string.substring(this.mark, this.lastIndex + 1); + } + + /** + * @return whether there is more text to read. + */ + public boolean canRead() { + return this.nextIndex < this.string.length(); + } + + public int getCurrentIndex() { + return this.lastIndex; + } +} diff --git a/src/main/java/kroppeb/stareval/parser/Tokenizer.java b/src/main/java/kroppeb/stareval/parser/Tokenizer.java new file mode 100644 index 000000000..7d1535022 --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/Tokenizer.java @@ -0,0 +1,107 @@ +package kroppeb.stareval.parser; + +import kroppeb.stareval.element.ExpressionElement; +import kroppeb.stareval.exception.ParseException; +import kroppeb.stareval.exception.UnexpectedCharacterException; +import kroppeb.stareval.exception.UnexpectedEndingException; + +class Tokenizer { + private Tokenizer() { + } + + static ExpressionElement parse(String input, ParserOptions options) throws ParseException { + return parseInternal(new StringReader(input), options); + } + + static ExpressionElement parseInternal(StringReader input, ParserOptions options) throws ParseException { + // parser stack + final Parser stack = new Parser(); + ParserOptions.TokenRules tokenRules = options.getTokenRules(); + + while (input.canRead()) { + input.skipWhitespace(); + + if (!input.canRead()) { + break; + } + + char c = input.read(); + + if (tokenRules.isIdStart(c)) { + final String id = readWhile(input, tokenRules::isIdPart); + stack.visitId(id); + } else if (c == '.' && stack.canReadAccess()) { + input.skipWhitespace(); + + if (input.canRead()) { + char start = input.read(); + + if (!tokenRules.isAccessStart(start)) { + throw new UnexpectedCharacterException("a valid accessor", start, input.getCurrentIndex()); + } + + final String access = readWhile(input, tokenRules::isAccessPart); + stack.visitAccess(access); + } else { + throw new UnexpectedEndingException("An expression can't end with '.'"); + } + } else if (tokenRules.isNumberStart(c)) { + // start parsing a number + final String numberString = readWhile(input, tokenRules::isNumberPart); + stack.visitNumber(numberString); + } else if (c == '(') { + stack.visitOpeningParenthesis(); + } else if (c == ',') { + stack.visitComma(input.getCurrentIndex()); + } else if (c == ')') { + stack.visitClosingParenthesis(input.getCurrentIndex()); + } else { + if (stack.canReadBinaryOp()) { + // maybe binary operator + OpResolver resolver = options.getBinaryOpResolver(c); + + if (resolver != null) { + stack.visitBinaryOperator(resolver.resolve(input)); + continue; + } + } else { + // maybe unary operator + OpResolver resolver = options.getUnaryOpResolver(c); + + if (resolver != null) { + stack.visitUnaryOperator(resolver.resolve(input)); + continue; + } + } + + throw new UnexpectedCharacterException(c, input.getCurrentIndex()); + } + } + + return stack.getFinal(input.getCurrentIndex()); + } + + /** + * The returned value includes the last value btw; + */ + private static String readWhile(StringReader input, CharPredicate predicate) { + input.mark(); + + while (input.canRead()) { + if (!predicate.test(input.peek())) { + break; + } + + // NB: Do not skip whitespace here implicitly. + input.skipOneCharacter(); + } + + return input.substring(); + } + + private interface CharPredicate { + boolean test(char c); + } +} + + diff --git a/src/main/java/kroppeb/stareval/parser/UnaryOp.java b/src/main/java/kroppeb/stareval/parser/UnaryOp.java new file mode 100644 index 000000000..150110288 --- /dev/null +++ b/src/main/java/kroppeb/stareval/parser/UnaryOp.java @@ -0,0 +1,14 @@ +package kroppeb.stareval.parser; + +public class UnaryOp { + private final String name; + + public UnaryOp(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java b/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java new file mode 100644 index 000000000..db33397b5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java @@ -0,0 +1,88 @@ +package me.jellysquid.mods.sodium.client; + +import com.gtnewhorizons.angelica.Tags; +import cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions; +import me.jellysquid.mods.sodium.proxy.CommonProxy; +import net.minecraft.client.Minecraft; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod(modid = SodiumClientMod.MODID, + name = SodiumClientMod.NAME, + version = Tags.VERSION, + acceptedMinecraftVersions = "[1.7.10]", + acceptableRemoteVersions = "*") +public class SodiumClientMod { + @SidedProxy(clientSide = "me.jellysquid.mods.sodium.proxy.ClientProxy", serverSide = "me.jellysquid.mods.sodium.proxy.CommonProxy") + public static CommonProxy proxy; + + private static SodiumGameOptions CONFIG; + public static Logger LOGGER = LogManager.getLogger("Embeddium"); + + private static String MOD_VERSION = Tags.VERSION; + + public static final String MODID = "embeddium"; + public static final String NAME = "Embeddium"; + + @Getter + private static Thread MainThread; + + public SodiumClientMod() { + MainThread = Thread.currentThread(); + } + + + + public static SodiumGameOptions options() { + if (CONFIG == null) { + CONFIG = loadConfig(); + } + + return CONFIG; + } + + public static Logger logger() { + if (LOGGER == null) { + LOGGER = LogManager.getLogger("Embeddium"); + } + + return LOGGER; + } + + private static SodiumGameOptions loadConfig() { + return SodiumGameOptions.load(Minecraft.getMinecraft().mcDataDir.toPath().resolve("config").resolve("rubidium-options.json")); + } + + public static String getVersion() { + if (MOD_VERSION == null) { + throw new NullPointerException("Mod version hasn't been populated yet"); + } + + return MOD_VERSION; + } + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + proxy.preInit(event); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + proxy.init(event); + } + + @Mod.EventHandler + public void postInit(FMLPostInitializationEvent event) { + proxy.postInit(event); + } + + public static boolean isDirectMemoryAccessEnabled() { + return options().advanced.allowDirectMemoryAccess; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/SodiumDebugScreenHandler.java b/src/main/java/me/jellysquid/mods/sodium/client/SodiumDebugScreenHandler.java new file mode 100644 index 000000000..2d9b9484b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/SodiumDebugScreenHandler.java @@ -0,0 +1,40 @@ +package me.jellysquid.mods.sodium.client; + +import cpw.mods.fml.common.eventhandler.EventPriority; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderBackend; +import net.minecraft.client.Minecraft; +import net.minecraftforge.client.event.RenderGameOverlayEvent; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; + +public class SodiumDebugScreenHandler { + public static final SodiumDebugScreenHandler INSTANCE = new SodiumDebugScreenHandler(); + @SubscribeEvent(priority = EventPriority.LOW) + public void onRenderGameOverlayTextEvent(RenderGameOverlayEvent.Text event) { + final Minecraft mc = Minecraft.getMinecraft(); + if (mc.gameSettings.showDebugInfo) { + event.right.add(2, "Off-Heap: +" + ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getUsed() / 1024L / 1024L + "MB"); + + event.right.add(""); + event.right.add("Sodium (Embeddium) Renderer"); + event.right.addAll(getChunkRendererDebugStrings()); + + } + } + + private static List getChunkRendererDebugStrings() { + ChunkRenderBackend backend = SodiumWorldRenderer.getInstance().getChunkRenderer(); + + List strings = new ArrayList<>(5); + strings.add("Chunk Renderer: " + backend.getRendererName()); + strings.add("Block Renderer: " + "Sodium"); + strings.addAll(backend.getDebugStrings()); + + return strings; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/GlObject.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/GlObject.java new file mode 100644 index 000000000..d75f94ece --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/GlObject.java @@ -0,0 +1,48 @@ +package me.jellysquid.mods.sodium.client.gl; + +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; + +/** + * An abstract object used to represent objects in OpenGL code safely. This class hides the direct handle to a OpenGL + * object, requiring that it first be checked by all callers to prevent null pointer de-referencing. However, this will + * not stop code from cloning the handle and trying to use it after it has been deleted and as such should not be + * relied on too heavily. + */ +public class GlObject { + private static final int INVALID_HANDLE = Integer.MIN_VALUE; + + protected final RenderDevice device; + private int handle = INVALID_HANDLE; + + public GlObject(RenderDevice owner) { + this.device = owner; + } + + protected final void setHandle(int handle) { + this.handle = handle; + } + + public final int handle() { + this.checkHandle(); + + return this.handle; + } + + protected final void checkHandle() { + if (!this.isHandleValid()) { + throw new IllegalStateException("Handle is not valid"); + } + } + + protected final boolean isHandleValid() { + return this.handle != INVALID_HANDLE; + } + + public final void invalidateHandle() { + this.handle = INVALID_HANDLE; + } + + public RenderDevice getDevice() { + return this.device; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java new file mode 100644 index 000000000..8d727b614 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java @@ -0,0 +1,169 @@ +package me.jellysquid.mods.sodium.client.gl.arena; + +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; + +import java.util.Set; + +public class GlBufferArena { + private static final GlBufferUsage BUFFER_USAGE = GlBufferUsage.GL_DYNAMIC_DRAW; + + private final RenderDevice device; + private final int resizeIncrement; + + private final Set freeRegions = new ObjectLinkedOpenHashSet<>(); + + private GlMutableBuffer vertexBuffer; + + private int position; + private int capacity; + private int allocCount; + + public GlBufferArena(RenderDevice device, int initialSize, int resizeIncrement) { + this.device = device; + + try (CommandList commands = device.createCommandList()) { + this.vertexBuffer = commands.createMutableBuffer(BUFFER_USAGE); + commands.allocateBuffer(GlBufferTarget.COPY_WRITE_BUFFER, this.vertexBuffer, initialSize); + } + + this.resizeIncrement = resizeIncrement; + this.capacity = initialSize; + } + + private void resize(CommandList commandList, int newCapacity) { + GlMutableBuffer src = this.vertexBuffer; + GlMutableBuffer dst = commandList.createMutableBuffer(BUFFER_USAGE); + + commandList.allocateBuffer(GlBufferTarget.COPY_WRITE_BUFFER, dst, newCapacity); + commandList.copyBufferSubData(src, dst, 0, 0, this.position); + commandList.deleteBuffer(src); + + this.vertexBuffer = dst; + this.capacity = newCapacity; + } + + public void prepareBuffer(CommandList commandList, int bytes) { + if (this.position + bytes >= this.capacity) { + this.resize(commandList, this.getNextSize(bytes)); + } + } + + public GlBufferSegment uploadBuffer(CommandList commandList, GlBuffer readBuffer, int readOffset, int byteCount) { + this.prepareBuffer(commandList, byteCount); + + GlBufferSegment segment = this.alloc(byteCount); + + commandList.copyBufferSubData(readBuffer, this.vertexBuffer, readOffset, segment.getStart(), byteCount); + + return segment; + } + + private int getNextSize(int len) { + return Math.max(this.capacity + this.resizeIncrement, this.capacity + len); + } + + public void free(GlBufferSegment segment) { + if (this.freeRegions.contains(segment)) { + throw new IllegalArgumentException("Segment already freed"); + } + + // Attempt merging + GlBufferSegment prev = null, next = null; + int selfEnd = segment.getEnd(); + for (GlBufferSegment freeSeg : this.freeRegions) { + if (prev != null && next != null) + break; + if (freeSeg.getStart() == selfEnd) { + next = freeSeg; + } else if (freeSeg.getEnd() == segment.getStart()) { + prev = freeSeg; + } + } + + if(prev != null || next != null) { + int start, end; + + if(prev != null) { + this.freeRegions.remove(prev); + start = prev.getStart(); + } else { + start = segment.getStart(); + } + + if(next != null) { + this.freeRegions.remove(next); + end = next.getEnd(); + } else { + end = segment.getEnd(); + } + + segment = new GlBufferSegment(this, start, end - start); + } + + this.freeRegions.add(segment); + + this.allocCount--; + } + + private GlBufferSegment alloc(int len) { + GlBufferSegment segment = this.allocReuse(len); + + if (segment == null) { + segment = new GlBufferSegment(this, this.position, len); + + this.position += len; + } + + this.allocCount++; + + return segment; + } + + private GlBufferSegment allocReuse(int len) { + GlBufferSegment bestSegment = null; + + for (GlBufferSegment segment : this.freeRegions) { + if (segment.getLength() < len) { + continue; + } + + if (bestSegment == null || bestSegment.getLength() > segment.getLength()) { + bestSegment = segment; + } + } + + if (bestSegment == null) { + return null; + } + + this.freeRegions.remove(bestSegment); + + int excess = bestSegment.getLength() - len; + + if (excess > 0) { + this.freeRegions.add(new GlBufferSegment(this, bestSegment.getStart() + len, excess)); + } + + return new GlBufferSegment(this, bestSegment.getStart(), len); + } + + public void delete() { + try (CommandList commands = this.device.createCommandList()) { + commands.deleteBuffer(this.vertexBuffer); + } + } + + public boolean isEmpty() { + return this.allocCount <= 0; + } + + public GlBuffer getBuffer() { + return this.vertexBuffer; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferSegment.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferSegment.java new file mode 100644 index 000000000..c9f4d3432 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferSegment.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.gl.arena; + +public class GlBufferSegment { + private final GlBufferArena arena; + private final int start; + private final int len; + + GlBufferSegment(GlBufferArena arena, int start, int len) { + this.arena = arena; + this.start = start; + this.len = len; + } + + public int getStart() { + return this.start; + } + + public int getLength() { + return this.len; + } + + public int getEnd() { + return this.start + this.len; + } + + public void delete() { + this.arena.free(this); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/array/GlVertexArray.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/array/GlVertexArray.java new file mode 100644 index 000000000..d72612acc --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/array/GlVertexArray.java @@ -0,0 +1,22 @@ +package me.jellysquid.mods.sodium.client.gl.array; + +import me.jellysquid.mods.sodium.client.gl.GlObject; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.func.GlFunctions; + +/** + * Provides Vertex Array functionality on supported platforms. + */ +public class GlVertexArray extends GlObject { + public static final int NULL_ARRAY_ID = 0; + + public GlVertexArray(RenderDevice owner) { + super(owner); + + if (!GlFunctions.isVertexArraySupported()) { + throw new UnsupportedOperationException("Vertex arrays are unsupported on this platform"); + } + + this.setHandle(GlFunctions.VERTEX_ARRAY.glGenVertexArrays()); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/BufferVertexFormat.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/BufferVertexFormat.java new file mode 100644 index 000000000..c29582ca8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/BufferVertexFormat.java @@ -0,0 +1,11 @@ +package me.jellysquid.mods.sodium.client.gl.attribute; + +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; + +public interface BufferVertexFormat { + static BufferVertexFormat from(VertexFormat format) { + return (BufferVertexFormat) format; + } + + int getStride(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttribute.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttribute.java new file mode 100644 index 000000000..0cadb2432 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttribute.java @@ -0,0 +1,55 @@ +package me.jellysquid.mods.sodium.client.gl.attribute; + +public class GlVertexAttribute { + private final int format; + private final int count; + private final int pointer; + private final int size; + private final int stride; + + private final boolean normalized; + + /** + * @param format The format used + * @param count The number of components in the vertex attribute + * @param normalized Specifies whether or not fixed-point data values should be normalized (true) or used directly + * as fixed-point values (false) + * @param pointer The offset to the first component in the attribute + */ + public GlVertexAttribute(GlVertexAttributeFormat format, int count, boolean normalized, int pointer, int stride) { + this(format.getGlFormat(), format.getSize() * count, count, normalized, pointer, stride); + } + + protected GlVertexAttribute(int format, int size, int count, boolean normalized, int pointer, int stride) { + this.format = format; + this.size = size; + this.count = count; + this.normalized = normalized; + this.pointer = pointer; + this.stride = stride; + } + + public int getSize() { + return this.size; + } + + public int getPointer() { + return this.pointer; + } + + public int getCount() { + return this.count; + } + + public int getFormat() { + return this.format; + } + + public boolean isNormalized() { + return this.normalized; + } + + public int getStride() { + return this.stride; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeBinding.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeBinding.java new file mode 100644 index 000000000..233808eaa --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeBinding.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.client.gl.attribute; + +import me.jellysquid.mods.sodium.client.gl.shader.ShaderBindingPoint; + +public class GlVertexAttributeBinding extends GlVertexAttribute { + private final int index; + + public GlVertexAttributeBinding(ShaderBindingPoint bindingPoint, GlVertexAttribute attribute) { + super(attribute.getFormat(), attribute.getSize(), attribute.getCount(), attribute.isNormalized(), attribute.getPointer(), attribute.getStride()); + + this.index = bindingPoint.getGenericAttributeIndex(); + } + + public int getIndex() { + return this.index; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeFormat.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeFormat.java new file mode 100644 index 000000000..11ba3aa27 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexAttributeFormat.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.client.gl.attribute; + +import org.lwjgl.opengl.GL11; + +/** + * An enumeration over the supported data types that can be used for vertex attributes. + */ +public class GlVertexAttributeFormat { + public static final GlVertexAttributeFormat FLOAT = new GlVertexAttributeFormat(GL11.GL_FLOAT, 4); + public static final GlVertexAttributeFormat UNSIGNED_SHORT = new GlVertexAttributeFormat(GL11.GL_UNSIGNED_SHORT, 2); + public static final GlVertexAttributeFormat UNSIGNED_BYTE = new GlVertexAttributeFormat(GL11.GL_UNSIGNED_BYTE, 1); + + private final int glId; + private final int size; + + public GlVertexAttributeFormat(int glId, int size) { + this.glId = glId; + this.size = size; + } + + public int getSize() { + return this.size; + } + + public int getGlFormat() { + return this.glId; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexFormat.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexFormat.java new file mode 100644 index 000000000..93465e1d0 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/attribute/GlVertexFormat.java @@ -0,0 +1,133 @@ +package me.jellysquid.mods.sodium.client.gl.attribute; + +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; + +import java.util.EnumMap; + +/** + * Provides a generic vertex format which contains the attributes defined by {@param T}. Other code can then retrieve + * the attributes and work with encoded data in a generic manner without needing to rely on a specific format. + * + * @param The enumeration over the vertex attributes + */ +public class GlVertexFormat> implements BufferVertexFormat { + // Iris + private static final GlVertexAttribute EMPTY = new GlVertexAttribute(GlVertexAttributeFormat.FLOAT, 0, false, 0, 0); + + private final Class attributeEnum; + private final EnumMap attributesKeyed; + private final GlVertexAttribute[] attributesArray; + + private final int stride; + + public GlVertexFormat(Class attributeEnum, EnumMap attributesKeyed, int stride) { + this.attributeEnum = attributeEnum; + this.attributesKeyed = attributesKeyed; + this.attributesArray = attributesKeyed.values().toArray(new GlVertexAttribute[0]); + this.stride = stride; + } + + public static > Builder builder(Class type, int stride) { + return new Builder<>(type, stride); + } + + /** + * Returns the {@link GlVertexAttribute} of this vertex format bound to the type {@param name}. + * @throws NullPointerException If the attribute does not exist in this format + */ + public GlVertexAttribute getAttribute(T name) { + GlVertexAttribute attr = this.attributesKeyed.get(name); + + if (attr == null) { + throw new NullPointerException("No attribute exists for " + name.toString()); + } + + return attr; + } + + /** + * @return The stride (or the size of) the vertex format in bytes + */ + public int getStride() { + return this.stride; + } + + @Override + public String toString() { + return String.format("GlVertexFormat<%s>{attributes=%d,stride=%d}", this.attributeEnum.getName(), + this.attributesKeyed.size(), this.stride); + } + + public GlVertexAttribute[] getAttributesArray() { + return this.attributesArray; + } + + public static class Builder> { + private final EnumMap attributes; + private final Class type; + private final int stride; + + public Builder(Class type, int stride) { + this.type = type; + this.attributes = new EnumMap<>(type); + this.stride = stride; + } + + public Builder addElement(T type, int pointer, GlVertexAttributeFormat format, int count, boolean normalized) { + return this.addElement(type, new GlVertexAttribute(format, count, normalized, pointer, this.stride)); + } + + /** + * Adds an vertex attribute which will be bound to the given generic attribute type. + * + * @param type The generic attribute type + * @param attribute The attribute to bind + * @throws IllegalStateException If an attribute is already bound to the generic type + */ + private Builder addElement(T type, GlVertexAttribute attribute) { + if ((attribute.getPointer() >= this.stride)) { + throw new IllegalArgumentException("Element starts outside vertex format"); + } + + if ((attribute.getPointer() + attribute.getSize() > this.stride)) { + throw new IllegalArgumentException("Element extends outside vertex format"); + } + + if (this.attributes.put(type, attribute) != null) { + throw new IllegalStateException("Generic attribute " + type.name() + " already defined in vertex format"); + } + + return this; + } + + /** + * Creates a {@link GlVertexFormat} from the current builder. + */ + public GlVertexFormat build() { + int size = 0; + + for (T key : this.type.getEnumConstants()) { + GlVertexAttribute attribute = this.attributes.get(key); + + if (attribute == null) { + if(key == ChunkMeshAttribute.NORMAL || key == ChunkMeshAttribute.TANGENT || key == ChunkMeshAttribute.MID_TEX_COORD || key == ChunkMeshAttribute.BLOCK_ID || key == ChunkMeshAttribute.MID_BLOCK) { + // Missing these attributes is acceptable and will be handled properly. + attribute = EMPTY; + } else { + throw new NullPointerException("Generic attribute not assigned to enumeration " + key.name()); + } + } + + size = Math.max(size, attribute.getPointer() + attribute.getSize()); + } + + // The stride must be large enough to cover all attributes. This still allows for additional padding + // to be added to the end of the vertex to accommodate alignment restrictions. + if (this.stride < size) { + throw new IllegalArgumentException("Stride is too small"); + } + + return new GlVertexFormat<>(this.type, this.attributes, this.stride); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBuffer.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBuffer.java new file mode 100644 index 000000000..0aa420a93 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBuffer.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import me.jellysquid.mods.sodium.client.gl.GlObject; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import org.lwjgl.opengl.GL15; + +public abstract class GlBuffer extends GlObject { + public static final int NULL_BUFFER_ID = 0; + + protected final GlBufferUsage usage; + + protected GlBuffer(RenderDevice owner, GlBufferUsage usage) { + super(owner); + + this.setHandle(GL15.glGenBuffers()); + + this.usage = usage; + } + + public GlBufferUsage getUsageHint() { + return this.usage; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTarget.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTarget.java new file mode 100644 index 000000000..8de59ec0d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTarget.java @@ -0,0 +1,31 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GL40; + +public enum GlBufferTarget { + ARRAY_BUFFER(GL15.GL_ARRAY_BUFFER, GL15.GL_ARRAY_BUFFER_BINDING), + COPY_READ_BUFFER(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_READ_BUFFER), + COPY_WRITE_BUFFER(GL31.GL_COPY_WRITE_BUFFER, GL31.GL_COPY_WRITE_BUFFER), + DRAW_INDIRECT_BUFFER(GL40.GL_DRAW_INDIRECT_BUFFER, GL40.GL_DRAW_INDIRECT_BUFFER_BINDING); + + public static final GlBufferTarget[] VALUES = GlBufferTarget.values(); + public static final int COUNT = VALUES.length; + + private final int target; + private final int binding; + + GlBufferTarget(int target, int binding) { + this.target = target; + this.binding = binding; + } + + public int getTargetParameter() { + return this.target; + } + + public int getBindingParameter() { + return this.binding; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferUsage.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferUsage.java new file mode 100644 index 000000000..d779dad5c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferUsage.java @@ -0,0 +1,25 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import org.lwjgl.opengl.GL15; + +public enum GlBufferUsage { + GL_STREAM_DRAW(GL15.GL_STREAM_DRAW), + GL_STREAM_READ(GL15.GL_STREAM_READ), + GL_STREAM_COPY(GL15.GL_STREAM_COPY), + GL_STATIC_DRAW(GL15.GL_STATIC_DRAW), + GL_STATIC_READ(GL15.GL_STATIC_READ), + GL_STATIC_COPY(GL15.GL_STATIC_COPY), + GL_DYNAMIC_DRAW(GL15.GL_DYNAMIC_DRAW), + GL_DYNAMIC_READ(GL15.GL_DYNAMIC_READ), + GL_DYNAMIC_COPY(GL15.GL_DYNAMIC_COPY); + + private final int id; + + GlBufferUsage(int id) { + this.id = id; + } + + public int getId() { + return this.id; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlMutableBuffer.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlMutableBuffer.java new file mode 100644 index 000000000..7f1ec5bb2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlMutableBuffer.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; + +/** + * A mutable buffer type which is supported with OpenGL 1.5+. The buffer's storage can be reallocated at any time + * without needing to re-create the buffer itself. + */ +public class GlMutableBuffer extends GlBuffer { + private long size = 0L; + + public GlMutableBuffer(RenderDevice owner, GlBufferUsage usage) { + super(owner, usage); + } + + public void setSize(long size) { + this.size = size; + } + + public long getSize() { + return this.size; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/VertexData.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/VertexData.java new file mode 100644 index 000000000..d27aba592 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/VertexData.java @@ -0,0 +1,18 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; + +import java.nio.ByteBuffer; + +/** + * Helper type for tagging the vertex format alongside the raw buffer data. + */ +public class VertexData { + public final GlVertexFormat format; + public final ByteBuffer buffer; + + public VertexData(ByteBuffer buffer, GlVertexFormat format) { + this.format = format; + this.buffer = buffer; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/compat/FogHelper.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/compat/FogHelper.java new file mode 100644 index 000000000..8254916fb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/compat/FogHelper.java @@ -0,0 +1,57 @@ +package me.jellysquid.mods.sodium.client.gl.compat; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkFogMode; +import org.lwjgl.opengl.GL11; + +public class FogHelper { + private static final float FAR_PLANE_THRESHOLD_EXP = (float) Math.log(1.0f / 0.0019f); + private static final float FAR_PLANE_THRESHOLD_EXP2 = (float) Math.sqrt(FAR_PLANE_THRESHOLD_EXP); + + public static float red, green, blue; + + public static float getFogEnd() { + return GLStateManager.getFogState().end; + } + + public static float getFogStart() { + return GLStateManager.getFogState().start; + } + + public static float getFogDensity() { + return GLStateManager.getFogState().density; + } + + /** + * Retrieves the current fog mode from the fixed-function pipeline. + */ + public static ChunkFogMode getFogMode() { + int mode = GLStateManager.getFogState().fogMode; + + if(mode == 0 || !GLStateManager.getFogState().mode.isEnabled()) + return ChunkFogMode.NONE; + + return switch (mode) { + case GL11.GL_EXP2, GL11.GL_EXP -> ChunkFogMode.EXP2; + case GL11.GL_LINEAR -> ChunkFogMode.LINEAR; + default -> throw new UnsupportedOperationException("Unknown fog mode: " + mode); + }; + } + + public static float getFogCutoff() { + int mode = GLStateManager.getFogState().fogMode; + + return switch (mode) { + case GL11.GL_LINEAR -> getFogEnd(); + case GL11.GL_EXP -> FAR_PLANE_THRESHOLD_EXP / getFogDensity(); + case GL11.GL_EXP2 -> FAR_PLANE_THRESHOLD_EXP2 / getFogDensity(); + default -> 0.0f; + }; + } + + public static float[] getFogColor() { + // TODO: Sodium +// return new float[]{BackgroundRenderer.red, BackgroundRenderer.green, BackgroundRenderer.blue, 1.0F}; + return new float[]{red, green, blue, 1.0F}; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/device/CommandList.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/CommandList.java new file mode 100644 index 000000000..f1678c6f7 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/CommandList.java @@ -0,0 +1,56 @@ +package me.jellysquid.mods.sodium.client.gl.device; + +import me.jellysquid.mods.sodium.client.gl.array.GlVertexArray; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlPrimitiveType; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.TessellationBinding; + +import java.nio.ByteBuffer; + +public interface CommandList extends AutoCloseable { + GlVertexArray createVertexArray(); + + GlMutableBuffer createMutableBuffer(GlBufferUsage usage); + + GlTessellation createTessellation(GlPrimitiveType primitiveType, TessellationBinding[] bindings); + + void bindVertexArray(GlVertexArray array); + + default void uploadData(GlMutableBuffer glBuffer, VertexData data) { + this.uploadData(glBuffer, data.buffer); + } + + void uploadData(GlMutableBuffer glBuffer, ByteBuffer byteBuffer); + + void copyBufferSubData(GlBuffer src, GlMutableBuffer dst, long readOffset, long writeOffset, long bytes); + + void bindBuffer(GlBufferTarget target, GlBuffer buffer); + + void unbindBuffer(GlBufferTarget target); + + void unbindVertexArray(); + + void invalidateBuffer(GlMutableBuffer glBuffer); + + void allocateBuffer(GlBufferTarget target, GlMutableBuffer buffer, long bufferSize); + + void deleteBuffer(GlBuffer buffer); + + void deleteVertexArray(GlVertexArray vertexArray); + + void flush(); + + DrawCommandList beginTessellating(GlTessellation tessellation); + + void deleteTessellation(GlTessellation tessellation); + + @Override + default void close() { + this.flush(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/device/DrawCommandList.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/DrawCommandList.java new file mode 100644 index 000000000..cc6b2e7bc --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/DrawCommandList.java @@ -0,0 +1,18 @@ +package me.jellysquid.mods.sodium.client.gl.device; + +import java.nio.IntBuffer; + +public interface DrawCommandList extends AutoCloseable { + void multiDrawArrays(IntBuffer first, IntBuffer count); + + void multiDrawArraysIndirect(long pointer, int count, int stride); + + void endTessellating(); + + void flush(); + + @Override + default void close() { + this.flush(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/device/GLRenderDevice.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/GLRenderDevice.java new file mode 100644 index 000000000..d7158596c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/GLRenderDevice.java @@ -0,0 +1,219 @@ +package me.jellysquid.mods.sodium.client.gl.device; + +import me.jellysquid.mods.sodium.client.gl.array.GlVertexArray; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.func.GlFunctions; +import me.jellysquid.mods.sodium.client.gl.state.GlStateTracker; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlFallbackTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlPrimitiveType; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlVertexArrayTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.TessellationBinding; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +public class GLRenderDevice implements RenderDevice { + private final GlStateTracker stateTracker = new GlStateTracker(); + private final CommandList commandList = new ImmediateCommandList(this.stateTracker); + private final DrawCommandList drawCommandList = new ImmediateDrawCommandList(); + + private boolean isActive; + private GlTessellation activeTessellation; + + @Override + public CommandList createCommandList() { + GLRenderDevice.this.checkDeviceActive(); + + return this.commandList; + } + + @Override + public void makeActive() { + if (this.isActive) { + return; + } + + this.stateTracker.clearRestoreState(); + this.isActive = true; + } + + @Override + public void makeInactive() { + if (!this.isActive) { + return; + } + + this.stateTracker.applyRestoreState(); + this.isActive = false; + } + + private void checkDeviceActive() { + if (!this.isActive) { + throw new IllegalStateException("Tried to access device from unmanaged context"); + } + } + + private class ImmediateCommandList implements CommandList { + private final GlStateTracker stateTracker; + + private ImmediateCommandList(GlStateTracker stateTracker) { + this.stateTracker = stateTracker; + } + + @Override + public void bindVertexArray(GlVertexArray array) { + if (this.stateTracker.makeVertexArrayActive(array)) { + GlFunctions.VERTEX_ARRAY.glBindVertexArray(array.handle()); + } + } + + @Override + public void uploadData(GlMutableBuffer glBuffer, ByteBuffer byteBuffer) { + this.bindBuffer(GlBufferTarget.ARRAY_BUFFER, glBuffer); + + GL15.glBufferData(GlBufferTarget.ARRAY_BUFFER.getTargetParameter(), byteBuffer, glBuffer.getUsageHint().getId()); + + glBuffer.setSize(byteBuffer.limit()); + } + + @Override + public void copyBufferSubData(GlBuffer src, GlMutableBuffer dst, long readOffset, long writeOffset, long bytes) { + if (writeOffset + bytes > dst.getSize()) { + throw new IllegalArgumentException("Not enough space in destination buffer (writeOffset + bytes > bufferSize)"); + } + + this.bindBuffer(GlBufferTarget.COPY_READ_BUFFER, src); + this.bindBuffer(GlBufferTarget.COPY_WRITE_BUFFER, dst); + + GlFunctions.BUFFER_COPY.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, readOffset, writeOffset, bytes); + } + + @Override + public void bindBuffer(GlBufferTarget target, GlBuffer buffer) { + if (this.stateTracker.makeBufferActive(target, buffer)) { + GL15.glBindBuffer(target.getTargetParameter(), buffer.handle()); + } + } + + @Override + public void unbindBuffer(GlBufferTarget target) { + if (this.stateTracker.makeBufferActive(target, null)) { + GL15.glBindBuffer(target.getTargetParameter(), GlBuffer.NULL_BUFFER_ID); + } + } + + @Override + public void unbindVertexArray() { + if (this.stateTracker.makeVertexArrayActive(null)) { + GlFunctions.VERTEX_ARRAY.glBindVertexArray(GlVertexArray.NULL_ARRAY_ID); + } + } + + @Override + public void invalidateBuffer(GlMutableBuffer glBuffer) { + this.allocateBuffer(GlBufferTarget.ARRAY_BUFFER, glBuffer, 0L); + } + + @Override + public void allocateBuffer(GlBufferTarget target, GlMutableBuffer buffer, long bufferSize) { + this.bindBuffer(target, buffer); + + GL15.glBufferData(target.getTargetParameter(), bufferSize, buffer.getUsageHint().getId()); + buffer.setSize(bufferSize); + } + + @Override + public void deleteBuffer(GlBuffer buffer) { + int handle = buffer.handle(); + buffer.invalidateHandle(); + + GL15.glDeleteBuffers(handle); + } + + @Override + public void deleteVertexArray(GlVertexArray array) { + int handle = array.handle(); + array.invalidateHandle(); + + GlFunctions.VERTEX_ARRAY.glDeleteVertexArrays(handle); + } + + @Override + public void flush() { + // NO-OP + } + + @Override + public DrawCommandList beginTessellating(GlTessellation tessellation) { + GLRenderDevice.this.activeTessellation = tessellation; + GLRenderDevice.this.activeTessellation.bind(GLRenderDevice.this.commandList); + + return GLRenderDevice.this.drawCommandList; + } + + @Override + public void deleteTessellation(GlTessellation tessellation) { + tessellation.delete(this); + } + + @Override + public GlVertexArray createVertexArray() { + return new GlVertexArray(GLRenderDevice.this); + } + + @Override + public GlMutableBuffer createMutableBuffer(GlBufferUsage usage) { + return new GlMutableBuffer(GLRenderDevice.this, usage); + } + + @Override + public GlTessellation createTessellation(GlPrimitiveType primitiveType, TessellationBinding[] bindings) { + if (GlVertexArrayTessellation.isSupported()) { + GlVertexArrayTessellation tessellation = new GlVertexArrayTessellation(new GlVertexArray(GLRenderDevice.this), primitiveType, bindings); + tessellation.init(this); + + return tessellation; + } else { + return new GlFallbackTessellation(primitiveType, bindings); + } + } + } + + private class ImmediateDrawCommandList implements DrawCommandList { + public ImmediateDrawCommandList() { + + } + + @Override + public void multiDrawArrays(IntBuffer first, IntBuffer count) { + GlPrimitiveType primitiveType = GLRenderDevice.this.activeTessellation.getPrimitiveType(); + GL14.glMultiDrawArrays(primitiveType.getId(), first, count); + } + + @Override + public void multiDrawArraysIndirect(long pointer, int count, int stride) { + GlPrimitiveType primitiveType = GLRenderDevice.this.activeTessellation.getPrimitiveType(); + GlFunctions.INDIRECT_DRAW.glMultiDrawArraysIndirect(primitiveType.getId(), pointer, count, stride); + } + + @Override + public void endTessellating() { + GLRenderDevice.this.activeTessellation.unbind(GLRenderDevice.this.commandList); + GLRenderDevice.this.activeTessellation = null; + } + + @Override + public void flush() { + if (GLRenderDevice.this.activeTessellation != null) { + this.endTessellating(); + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/device/RenderDevice.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/RenderDevice.java new file mode 100644 index 000000000..ffbf2a8e5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/device/RenderDevice.java @@ -0,0 +1,18 @@ +package me.jellysquid.mods.sodium.client.gl.device; + +public interface RenderDevice { + RenderDevice INSTANCE = new GLRenderDevice(); + + CommandList createCommandList(); + + static void enterManagedCode() { + RenderDevice.INSTANCE.makeActive(); + } + + static void exitManagedCode() { + RenderDevice.INSTANCE.makeInactive(); + } + + void makeActive(); + void makeInactive(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlBufferCopyFunctions.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlBufferCopyFunctions.java new file mode 100644 index 000000000..1ea1d8501 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlBufferCopyFunctions.java @@ -0,0 +1,41 @@ +package me.jellysquid.mods.sodium.client.gl.func; + +import org.lwjgl.opengl.ARBCopyBuffer; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GL31; + +/** + * Requires OpenGL 3.1+ or the ARB_copy_buffer extension. + */ +public enum GlBufferCopyFunctions { + CORE { + @Override + public void glCopyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size) { + GL31.glCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size); + } + }, + ARB { + @Override + public void glCopyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size) { + ARBCopyBuffer.glCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size); + } + }, + UNSUPPORTED { + @Override + public void glCopyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size) { + throw new UnsupportedOperationException(); + } + }; + + static GlBufferCopyFunctions load(ContextCapabilities capabilities) { + if (capabilities.OpenGL31) { + return GlBufferCopyFunctions.CORE; + } else if (capabilities.GL_ARB_copy_buffer) { + return GlBufferCopyFunctions.ARB; + } else { + return GlBufferCopyFunctions.UNSUPPORTED; + } + } + + public abstract void glCopyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlFunctions.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlFunctions.java new file mode 100644 index 000000000..3ee84b09c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlFunctions.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.gl.func; + +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GLContext; + +public class GlFunctions { + private static final ContextCapabilities capabilities = GLContext.getCapabilities(); + + public static final GlVertexArrayFunctions VERTEX_ARRAY = GlVertexArrayFunctions.load(capabilities); + public static final GlBufferCopyFunctions BUFFER_COPY = GlBufferCopyFunctions.load(capabilities); + public static final GlIndirectMultiDrawFunctions INDIRECT_DRAW = GlIndirectMultiDrawFunctions.load(capabilities); + public static final GlInstancedArrayFunctions INSTANCED_ARRAY = GlInstancedArrayFunctions.load(capabilities); + + public static boolean isVertexArraySupported() { + return VERTEX_ARRAY != GlVertexArrayFunctions.UNSUPPORTED; + } + + public static boolean isBufferCopySupported() { + return BUFFER_COPY != GlBufferCopyFunctions.UNSUPPORTED; + } + + public static boolean isIndirectMultiDrawSupported() { + return INDIRECT_DRAW != GlIndirectMultiDrawFunctions.UNSUPPORTED; + } + + public static boolean isInstancedArraySupported() { + return INSTANCED_ARRAY != GlInstancedArrayFunctions.UNSUPPORTED; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlIndirectMultiDrawFunctions.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlIndirectMultiDrawFunctions.java new file mode 100644 index 000000000..d706c3d15 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlIndirectMultiDrawFunctions.java @@ -0,0 +1,38 @@ +package me.jellysquid.mods.sodium.client.gl.func; + +import org.lwjgl.opengl.ARBMultiDrawIndirect; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GL43; + +public enum GlIndirectMultiDrawFunctions { + CORE { + @Override + public void glMultiDrawArraysIndirect(int mode, long indirect, int primcount, int stride) { + GL43.glMultiDrawArraysIndirect(mode, indirect, primcount, stride); + } + }, + ARB { + @Override + public void glMultiDrawArraysIndirect(int mode, long indirect, int primcount, int stride) { + ARBMultiDrawIndirect.glMultiDrawArraysIndirect(mode, indirect, primcount, stride); + } + }, + UNSUPPORTED { + @Override + public void glMultiDrawArraysIndirect(int mode, long indirect, int primcount, int stride) { + throw new UnsupportedOperationException(); + } + }; + + public static GlIndirectMultiDrawFunctions load(ContextCapabilities capabilities) { + if (capabilities.OpenGL43) { + return CORE; + } else if (capabilities.GL_ARB_multi_draw_indirect && capabilities.GL_ARB_draw_indirect) { + return ARB; + } else { + return UNSUPPORTED; + } + } + + public abstract void glMultiDrawArraysIndirect(int mode, long indirect, int primcount, int stride); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlInstancedArrayFunctions.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlInstancedArrayFunctions.java new file mode 100644 index 000000000..4ec13d7e4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlInstancedArrayFunctions.java @@ -0,0 +1,38 @@ +package me.jellysquid.mods.sodium.client.gl.func; + +import org.lwjgl.opengl.ARBInstancedArrays; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GL33; + +public enum GlInstancedArrayFunctions { + CORE { + @Override + public void glVertexAttribDivisor(int index, int divisor) { + GL33.glVertexAttribDivisor(index, divisor); + } + }, + ARB { + @Override + public void glVertexAttribDivisor(int index, int divisor) { + ARBInstancedArrays.glVertexAttribDivisorARB(index, divisor); + } + }, + UNSUPPORTED { + @Override + public void glVertexAttribDivisor(int index, int divisor) { + throw new UnsupportedOperationException(); + } + }; + + public static GlInstancedArrayFunctions load(ContextCapabilities capabilities) { + if (capabilities.OpenGL33) { + return CORE; + } else if (capabilities.GL_ARB_instanced_arrays) { + return ARB; + } else { + return UNSUPPORTED; + } + } + + public abstract void glVertexAttribDivisor(int index, int divisor); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlVertexArrayFunctions.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlVertexArrayFunctions.java new file mode 100644 index 000000000..1a9bab73b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/func/GlVertexArrayFunctions.java @@ -0,0 +1,75 @@ +package me.jellysquid.mods.sodium.client.gl.func; + +import org.lwjgl.opengl.ARBVertexArrayObject; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GL30; + +/** + * Requires OpenGL 3.0+ or the ARB_vertex_array_object extension. + */ +public enum GlVertexArrayFunctions { + BASE { + @Override + public void glBindVertexArray(int id) { + GL30.glBindVertexArray(id); + } + + @Override + public int glGenVertexArrays() { + return GL30.glGenVertexArrays(); + } + + @Override + public void glDeleteVertexArrays(int id) { + GL30.glDeleteVertexArrays(id); + } + }, + ARB { + @Override + public void glBindVertexArray(int id) { + ARBVertexArrayObject.glBindVertexArray(id); + } + + @Override + public int glGenVertexArrays() { + return ARBVertexArrayObject.glGenVertexArrays(); + } + + @Override + public void glDeleteVertexArrays(int id) { + ARBVertexArrayObject.glDeleteVertexArrays(id); + } + }, + UNSUPPORTED { + @Override + public void glBindVertexArray(int id) { + throw new UnsupportedOperationException(); + } + + @Override + public int glGenVertexArrays() { + throw new UnsupportedOperationException(); + } + + @Override + public void glDeleteVertexArrays(int id) { + throw new UnsupportedOperationException(); + } + }; + + static GlVertexArrayFunctions load(ContextCapabilities capabilities) { + if (capabilities.OpenGL30) { + return BASE; + } else if (capabilities.GL_ARB_vertex_array_object) { + return ARB; + } + + return UNSUPPORTED; + } + + public abstract void glBindVertexArray(int id); + + public abstract int glGenVertexArrays(); + + public abstract void glDeleteVertexArrays(int id); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlProgram.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlProgram.java new file mode 100644 index 000000000..c7a8618fb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlProgram.java @@ -0,0 +1,117 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +import me.jellysquid.mods.sodium.client.gl.GlObject; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + + +/** + * An OpenGL shader program. + */ +public abstract class GlProgram extends GlObject { + private static final Logger LOGGER = LogManager.getLogger(GlProgram.class); + + private final ResourceLocation name; + + protected GlProgram(RenderDevice owner, ResourceLocation name, int program) { + super(owner); + + this.name = name; + this.setHandle(program); + } + + public static Builder builder(ResourceLocation identifier) { + return new Builder(identifier); + } + + public void bind() { + GL20.glUseProgram(this.handle()); + } + + public void unbind() { + GL20.glUseProgram(0); + } + + public ResourceLocation getName() { + return this.name; + } + + /** + * Retrieves the index of the uniform with the given name. + * @param name The name of the uniform to find the index of + * @return The uniform's index + * @throws NullPointerException If no uniform exists with the given name + */ + public int getUniformLocation(String name) { + int index = GL20.glGetUniformLocation(this.handle(), name); + + if (index < 0) { + throw new NullPointerException("No uniform exists with name: " + name); + } + + return index; + } + + public void delete() { + GL20.glDeleteProgram(this.handle()); + + this.invalidateHandle(); + } + + public static class Builder { + private final ResourceLocation name; + private final int program; + + public Builder(ResourceLocation name) { + this.name = name; + this.program = GL20.glCreateProgram(); + } + + public Builder attachShader(GlShader shader) { + GL20.glAttachShader(this.program, shader.handle()); + + return this; + } + + /** + * Links the attached shaders to this program and returns a user-defined container which wraps the shader + * program. This container can, for example, provide methods for updating the specific uniforms of that shader + * set. + * + * @param factory The factory which will create the shader program's container + * @param

The type which should be instantiated with the new program's handle + * @return An instantiated shader container as provided by the factory + */ + public

P build(ProgramFactory

factory) { + GL20.glLinkProgram(this.program); + + String log = GL20.glGetProgramInfoLog(this.program, GL20.GL_INFO_LOG_LENGTH); + + if (!log.isEmpty()) { + LOGGER.warn("Program link log for " + this.name + ": " + log); + } + + int result = GL20.glGetProgrami(this.program, GL20.GL_LINK_STATUS); + + if (result != GL11.GL_TRUE) { + throw new RuntimeException("Shader program linking failed, see log for details"); + } + + return factory.create(this.name, this.program); + } + + public Builder bindAttribute(String name, ShaderBindingPoint binding) { + GL20.glBindAttribLocation(this.program, binding.getGenericAttributeIndex(), name); + + return this; + } + } + + public interface ProgramFactory

{ + P create(ResourceLocation name, int handle); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlShader.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlShader.java new file mode 100644 index 000000000..97a2ecc52 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/GlShader.java @@ -0,0 +1,92 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +import me.jellysquid.mods.sodium.client.gl.GlObject; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +/** + * A compiled OpenGL shader object. + */ +public class GlShader extends GlObject { + private static final Logger LOGGER = LogManager.getLogger(GlShader.class); + + private final ResourceLocation name; + + public GlShader(RenderDevice owner, ShaderType type, ResourceLocation name, String src, ShaderConstants constants) { + super(owner); + + this.name = name; + + src = processShader(src, constants); + + int handle = GL20.glCreateShader(type.id); + // TODO: ShaderWorkaround + GL20.glShaderSource(handle, src); + GL20.glCompileShader(handle); + + String log = GL20.glGetShaderInfoLog(handle, GL20.GL_INFO_LOG_LENGTH); + + if (!log.isEmpty()) { + LOGGER.warn("Shader compilation log for " + this.name + ": " + log); + } + + int result = GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS); + + if (result != GL11.GL_TRUE) { + throw new RuntimeException("Shader compilation failed, see log for details"); + } + + this.setHandle(handle); + } + + /** + * Adds an additional list of defines to the top of a GLSL shader file just after the version declaration. This + * allows for ghetto shader specialization. + */ + private static String processShader(String src, ShaderConstants constants) { + StringBuilder builder = new StringBuilder(src.length()); + boolean patched = false; + + try (BufferedReader reader = new BufferedReader(new StringReader(src))) { + String line; + + while ((line = reader.readLine()) != null) { + // Write the line out to the patched GLSL code string + builder.append(line).append("\n"); + + // Now, see if the line we just wrote declares the version + // If we haven't already added our define declarations, add them just after the version declaration + if (!patched && line.startsWith("#version")) { + for (String macro : constants.getDefineStrings()) { + builder.append(macro).append('\n'); + } + + // We did our work, don't add them again + patched = true; + } + } + } catch (IOException e) { + throw new RuntimeException("Could not process shader source", e); + } + + return builder.toString(); + } + + public ResourceLocation getName() { + return this.name; + } + + public void delete() { + GL20.glDeleteShader(this.handle()); + + this.invalidateHandle(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderBindingPoint.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderBindingPoint.java new file mode 100644 index 000000000..4da44924f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderBindingPoint.java @@ -0,0 +1,13 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +public class ShaderBindingPoint { + private final int genericAttributeIndex; + + public ShaderBindingPoint(int genericAttributeIndex) { + this.genericAttributeIndex = genericAttributeIndex; + } + + public int getGenericAttributeIndex() { + return genericAttributeIndex; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderConstants.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderConstants.java new file mode 100644 index 000000000..c8ba2caef --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderConstants.java @@ -0,0 +1,74 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ShaderConstants { + private final List defines; + + private ShaderConstants(List defines) { + this.defines = defines; + } + + public List getDefineStrings() { + return this.defines; + } + + public static ShaderConstants fromStringList(List defines) { + Builder builder = new Builder(); + + for (String define : defines) { + builder.define(define); + } + + return builder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private static final String EMPTY_VALUE = ""; + + private final HashMap constants = new HashMap<>(); + + private Builder() { + + } + + public void define(String name) { + this.define(name, EMPTY_VALUE); + } + + public void define(String name, String value) { + String prev = this.constants.get(name); + + if (prev != null) { + throw new IllegalArgumentException("Constant " + name + " is already defined with value " + prev); + } + + this.constants.put(name, value); + } + + public ShaderConstants build() { + List defines = new ArrayList<>(this.constants.size()); + + for (Map.Entry entry : this.constants.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + if (value.length() <= 0) { + defines.add("#define " + key); + } else { + defines.add("#define " + key + " " + value); + } + } + + return new ShaderConstants(Collections.unmodifiableList(defines)); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderLoader.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderLoader.java new file mode 100644 index 000000000..eca711854 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderLoader.java @@ -0,0 +1,52 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class ShaderLoader { + /** + * Creates an OpenGL shader from GLSL sources. The GLSL source file should be made available on the classpath at the + * path of `/assets/{namespace}/shaders/{path}`. User defines can be used to declare variables in the shader source + * after the version header, allowing for conditional compilation with macro code. + * + * + * @param device + * @param type The type of shader to create + * @param name The ResourceLocation used to locate the shader source file + * @param constants A list of constants for shader specialization + * @return An OpenGL shader object compiled with the given user defines + */ + public static GlShader loadShader(RenderDevice device, ShaderType type, ResourceLocation name, ShaderConstants constants) { + return new GlShader(device, type, name, getShaderSource(getShaderPath(name, type)), constants); + } + + /** + * Use {@link ShaderLoader#loadShader(RenderDevice, ShaderType, ResourceLocation, ShaderConstants)} instead. This will be removed. + */ + @Deprecated + public static GlShader loadShader(RenderDevice device, ShaderType type, ResourceLocation name, List constants) { + return new GlShader(device, type, name, getShaderSource(getShaderPath(name, type)), ShaderConstants.fromStringList(constants)); + } + + public static String getShaderPath(ResourceLocation name, ShaderType type) { + return String.format("/assets/%s/shaders/%s.%s.glsl", name.getResourceDomain(), name.getResourcePath(), type == ShaderType.VERTEX ? "v" : "f"); + } + + private static String getShaderSource(String path) { + try (InputStream in = ShaderLoader.class.getResourceAsStream(path)) { + if (in == null) { + throw new RuntimeException("Shader not found: " + path); + } + + return IOUtils.toString(in, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Could not read shader sources", e); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderType.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderType.java new file mode 100644 index 000000000..23d16313b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderType.java @@ -0,0 +1,19 @@ +package me.jellysquid.mods.sodium.client.gl.shader; + +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL32; + +/** + * An enumeration over the supported OpenGL shader types. + */ +public enum ShaderType { + VERTEX(GL20.GL_VERTEX_SHADER), + FRAGMENT(GL20.GL_FRAGMENT_SHADER), + GEOMETRY(GL32.GL_GEOMETRY_SHADER); + + public final int id; + + ShaderType(int id) { + this.id = id; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/state/GlStateTracker.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/state/GlStateTracker.java new file mode 100644 index 000000000..010b8fc86 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/state/GlStateTracker.java @@ -0,0 +1,63 @@ +package me.jellysquid.mods.sodium.client.gl.state; + +import me.jellysquid.mods.sodium.client.gl.array.GlVertexArray; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; + +import java.util.Arrays; + +public class GlStateTracker { + private static final int UNASSIGNED_HANDLE = -1; + + private final int[] bufferState = new int[GlBufferTarget.COUNT]; + //private final int[] bufferRestoreState = new int[GlBufferTarget.COUNT]; + + private int vertexArrayState; + //private int vertexArrayRestoreState; + + public GlStateTracker() { + this.clearRestoreState(); + } + + public boolean makeBufferActive(GlBufferTarget target, GlBuffer buffer) { + return this.makeBufferActive(target, buffer == null ? GlBuffer.NULL_BUFFER_ID : buffer.handle()); + } + + private boolean makeBufferActive(GlBufferTarget target, int buffer) { + int prevBuffer = this.bufferState[target.ordinal()]; + + this.bufferState[target.ordinal()] = buffer; + + return prevBuffer != buffer; + } + + public boolean makeVertexArrayActive(GlVertexArray array) { + return this.makeVertexArrayActive(array == null ? GlVertexArray.NULL_ARRAY_ID : array.handle()); + } + + private boolean makeVertexArrayActive(int array) { + int prevArray = this.vertexArrayState; + + this.vertexArrayState = array; + + return prevArray != array; + } + + public void applyRestoreState() { + for (int i = 0; i < GlBufferTarget.COUNT; i++) { + GL15.glBindBuffer(GlBufferTarget.VALUES[i].getTargetParameter(), 0); + } + + GL30.glBindVertexArray(0); + } + + public void clearRestoreState() { + Arrays.fill(this.bufferState, UNASSIGNED_HANDLE); + //Arrays.fill(this.bufferRestoreState, UNASSIGNED_HANDLE); + + this.vertexArrayState = UNASSIGNED_HANDLE; + //this.vertexArrayRestoreState = UNASSIGNED_HANDLE; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlAbstractTessellation.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlAbstractTessellation.java new file mode 100644 index 000000000..95a46cb7a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlAbstractTessellation.java @@ -0,0 +1,37 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.func.GlFunctions; +import org.lwjgl.opengl.GL20; + +public abstract class GlAbstractTessellation implements GlTessellation { + protected final GlPrimitiveType primitiveType; + protected final TessellationBinding[] bindings; + + protected GlAbstractTessellation(GlPrimitiveType primitiveType, TessellationBinding[] bindings) { + this.primitiveType = primitiveType; + this.bindings = bindings; + } + + @Override + public GlPrimitiveType getPrimitiveType() { + return this.primitiveType; + } + + protected void bindAttributes(CommandList commandList) { + for (TessellationBinding binding : this.bindings) { + commandList.bindBuffer(GlBufferTarget.ARRAY_BUFFER, binding.getBuffer()); + + for (GlVertexAttributeBinding attrib : binding.getAttributeBindings()) { + GL20.glVertexAttribPointer(attrib.getIndex(), attrib.getCount(), attrib.getFormat(), attrib.isNormalized(), attrib.getStride(), attrib.getPointer()); + GL20.glEnableVertexAttribArray(attrib.getIndex()); + + if (binding.isInstanced()) { + GlFunctions.INSTANCED_ARRAY.glVertexAttribDivisor(attrib.getIndex(), 1); + } + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlFallbackTessellation.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlFallbackTessellation.java new file mode 100644 index 000000000..2cb9531d2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlFallbackTessellation.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import org.lwjgl.opengl.GL20; + +public class GlFallbackTessellation extends GlAbstractTessellation { + public GlFallbackTessellation(GlPrimitiveType primitiveType, TessellationBinding[] bindings) { + super(primitiveType, bindings); + } + + @Override + public void delete(CommandList commandList) { + + } + + @Override + public void bind(CommandList commandList) { + this.bindAttributes(commandList); + } + + @Override + public void unbind(CommandList commandList) { + for (TessellationBinding binding : this.bindings) { + for (GlVertexAttributeBinding attrib : binding.getAttributeBindings()) { + GL20.glDisableVertexAttribArray(attrib.getIndex()); + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlPrimitiveType.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlPrimitiveType.java new file mode 100644 index 000000000..20d4aa72b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlPrimitiveType.java @@ -0,0 +1,19 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import org.lwjgl.opengl.GL11; + +public enum GlPrimitiveType { + LINES(GL11.GL_LINES), + TRIANGLES(GL11.GL_TRIANGLES), + QUADS(GL11.GL_QUADS); + + private final int id; + + GlPrimitiveType(int id) { + this.id = id; + } + + public int getId() { + return this.id; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlTessellation.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlTessellation.java new file mode 100644 index 000000000..cf7aeb70a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlTessellation.java @@ -0,0 +1,13 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import me.jellysquid.mods.sodium.client.gl.device.CommandList; + +public interface GlTessellation { + void delete(CommandList commandList); + + void bind(CommandList commandList); + + void unbind(CommandList commandList); + + GlPrimitiveType getPrimitiveType(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlVertexArrayTessellation.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlVertexArrayTessellation.java new file mode 100644 index 000000000..f09f3bfef --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/GlVertexArrayTessellation.java @@ -0,0 +1,40 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import me.jellysquid.mods.sodium.client.gl.array.GlVertexArray; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.func.GlFunctions; + +public class GlVertexArrayTessellation extends GlAbstractTessellation { + private final GlVertexArray array; + + public GlVertexArrayTessellation(GlVertexArray array, GlPrimitiveType primitiveType, TessellationBinding[] bindings) { + super(primitiveType, bindings); + + this.array = array; + } + + public static boolean isSupported() { + return GlFunctions.isVertexArraySupported(); + } + + public void init(CommandList commandList) { + this.bind(commandList); + this.bindAttributes(commandList); + this.unbind(commandList); + } + + @Override + public void delete(CommandList commandList) { + commandList.deleteVertexArray(this.array); + } + + @Override + public void bind(CommandList commandList) { + commandList.bindVertexArray(this.array); + } + + @Override + public void unbind(CommandList commandList) { + commandList.unbindVertexArray(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/TessellationBinding.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/TessellationBinding.java new file mode 100644 index 000000000..b383d74e6 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/tessellation/TessellationBinding.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.client.gl.tessellation; + +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; + +public class TessellationBinding { + private final GlBuffer buffer; + private final GlVertexAttributeBinding[] bindings; + private final boolean instanced; + + public TessellationBinding(GlBuffer buffer, GlVertexAttributeBinding[] bindings, boolean instanced) { + this.buffer = buffer; + this.bindings = bindings; + this.instanced = instanced; + } + + public GlBuffer getBuffer() { + return this.buffer; + } + + public GlVertexAttributeBinding[] getAttributeBindings() { + return this.bindings; + } + + public boolean isInstanced() { + return this.instanced; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/util/BufferSlice.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/util/BufferSlice.java new file mode 100644 index 000000000..898d3f9ca --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/util/BufferSlice.java @@ -0,0 +1,22 @@ +package me.jellysquid.mods.sodium.client.gl.util; + +public class BufferSlice { + public final int start, len; + + public BufferSlice(int start, int len) { + this.start = start; + this.len = len; + } + + public static long pack(int start, int len) { + return (long) start & 0xffffffffL | ((long) len & 0xffffffffL) << 32; + } + + public static int unpackStart(long slice) { + return (int) (slice & 0xffffffffL); + } + + public static int unpackLength(long slice) { + return (int) (slice >>> 32 & 0xffffffffL); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/util/GlMultiDrawBatch.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/util/GlMultiDrawBatch.java new file mode 100644 index 000000000..d38ee8d2d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/util/GlMultiDrawBatch.java @@ -0,0 +1,68 @@ +package me.jellysquid.mods.sodium.client.gl.util; + +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL20; + + +import java.nio.IntBuffer; + +/** + * Provides a fixed-size queue for batching draw calls for vertex data in the same buffer. This internally + * uses {@link GL20#glMultiDrawArrays(int, IntBuffer, IntBuffer)} and should be compatible on any relevant platform. + */ +public class GlMultiDrawBatch { + private IntBuffer bufIndices; + private IntBuffer bufLen; + private int count; + private boolean isBuilding; + + public GlMultiDrawBatch(int capacity) { + this.bufIndices = BufferUtils.createIntBuffer(capacity); + this.bufLen = BufferUtils.createIntBuffer(capacity); + } + + public IntBuffer getIndicesBuffer() { + return this.bufIndices; + } + + public IntBuffer getLengthBuffer() { + return this.bufLen; + } + + public void begin() { + this.bufIndices.clear(); + this.bufLen.clear(); + this.count = 0; + + this.isBuilding = true; + } + + public void end() { + this.bufIndices.limit(this.count); + this.bufLen.limit(this.count); + + this.isBuilding = false; + } + + public boolean isEmpty() { + return this.count <= 0; + } + + public void addChunkRender(int first, int count) { + int i = this.count++; + this.bufIndices.put(i, first); + this.bufLen.put(i, count); + } + + public boolean isBuilding() { + return this.isBuilding; + } + + public void delete() { + this.bufIndices.clear(); + this.bufLen.clear(); + + this.bufIndices = null; + this.bufLen = null; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java new file mode 100644 index 000000000..f8b4d5fa2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java @@ -0,0 +1,412 @@ +package me.jellysquid.mods.sodium.client.gui; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import jss.notfine.core.Settings; +import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; +import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpact; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpl; +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl; +import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl; +import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsMode; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality; +import me.jellysquid.mods.sodium.client.gui.options.named.LightingQuality; +import me.jellysquid.mods.sodium.client.gui.options.named.ParticleMode; +import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; +import me.jellysquid.mods.sodium.client.gui.options.storage.SodiumOptionsStorage; +import me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw.MultidrawChunkRenderBackend; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.option.IrisVideoSettings; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.settings.GameSettings; +import org.lwjgl.opengl.Display; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SodiumGameOptionPages { + private static final SodiumOptionsStorage sodiumOpts = new SodiumOptionsStorage(); + private static final MinecraftOptionsStorage vanillaOpts = new MinecraftOptionsStorage(); + + public static OptionPage general() { + final List groups = new ArrayList<>(); + final OptionGroup.Builder firstGroupBuilder = OptionGroup.createBuilder(); + + firstGroupBuilder.add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.renderDistance")) + .setTooltip(I18n.format("sodium.options.view_distance.tooltip")) + .setControl(option -> new SliderControl(option, 2, (int) GameSettings.Options.RENDER_DISTANCE.getValueMax(), 1, ControlValueFormatter.quantity("options.chunks"))) + .setBinding((options, value) -> options.renderDistanceChunks = value, options -> options.renderDistanceChunks) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()); + + if(AngelicaConfig.enableIris) { + final OptionImpl maxShadowDistanceSlider = OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.iris.shadowDistance")) + .setTooltip(I18n.format("options.iris.shadowDistance.sodium_tooltip")) + .setControl(option -> new SliderControl(option, 0, 32, 1, ControlValueFormatter.quantity("options.chunks"))) + .setBinding((options, value) -> { + IrisVideoSettings.shadowDistance = value; + try { + Iris.getIrisConfig().save(); + } catch (IOException e) { + e.printStackTrace(); + } + }, + options -> IrisVideoSettings.getOverriddenShadowDistance(IrisVideoSettings.shadowDistance)) + .setImpact(OptionImpact.HIGH) + .setEnabled(true) + .build(); + + maxShadowDistanceSlider.iris$dynamicallyEnable(IrisVideoSettings::isShadowDistanceSliderEnabled); + firstGroupBuilder.add(maxShadowDistanceSlider).build(); + } + + firstGroupBuilder.add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.gamma")) + .setTooltip(I18n.format("sodium.options.brightness.tooltip")) + .setControl(opt -> new SliderControl(opt, 0, 100, 1, ControlValueFormatter.brightness())) + .setBinding((opts, value) -> opts.gammaSetting = value * 0.01F, (opts) -> (int) (opts.gammaSetting / 0.01F)) + .build()); + firstGroupBuilder.add(Settings.MODE_SKY.option); + firstGroupBuilder.add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("sodium.options.clouds.name")) + .setTooltip(I18n.format("sodium.options.clouds.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.clouds = value, (opts) -> opts.clouds) + .setImpact(OptionImpact.LOW) + .build()); + groups.add(firstGroupBuilder.build()); + + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.guiScale")) + .setTooltip(I18n.format("sodium.options.gui_scale.tooltip")) + .setControl(option -> new SliderControl(option, 0, 3, 1, ControlValueFormatter.guiScale())) + .setBinding((opts, value) -> { + opts.guiScale = value; + // Resizing our window + if(Minecraft.getMinecraft().currentScreen instanceof SodiumOptionsGUI oldGui) { + Minecraft.getMinecraft().displayGuiScreen(new SodiumOptionsGUI(oldGui.prevScreen)); + } + }, opts -> opts.guiScale) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.fullscreen")) + .setTooltip(I18n.format("sodium.options.fullscreen.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> { + opts.fullScreen = value; + + final Minecraft client = Minecraft.getMinecraft(); + + if (client.isFullScreen() != opts.fullScreen) { + client.toggleFullscreen(); + + // The client might not be able to enter full-screen mode + opts.fullScreen = client.isFullScreen(); + } + }, (opts) -> opts.fullScreen) + .build()) + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.vsync")) + .setTooltip(I18n.format("sodium.options.v_sync.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> { + opts.enableVsync = value; + Display.setVSyncEnabled(opts.enableVsync); + }, opts -> opts.enableVsync) + .setImpact(OptionImpact.VARIES) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.framerateLimit")) + .setTooltip(I18n.format("sodium.options.fps_limit.tooltip")) + .setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit())) + .setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate) + .build()) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(I18n.format("options.viewBobbing")) + .setTooltip(I18n.format("sodium.options.view_bobbing.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.viewBobbing = value, opts -> opts.viewBobbing) + .build()) + .build()); + + return new OptionPage(I18n.format("stat.generalButton"), ImmutableList.copyOf(groups)); + } + + public static OptionPage quality() { + final List groups = new ArrayList<>(); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(GraphicsMode.class, vanillaOpts) + .setName(I18n.format("options.graphics")) + .setTooltip(I18n.format("sodium.options.graphics_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsMode.class)) + .setBinding( + (opts, value) -> opts.fancyGraphics = value.isFancy(), + opts -> GraphicsMode.fromBoolean(opts.fancyGraphics)) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("options.renderClouds")) + .setTooltip(I18n.format("sodium.options.clouds_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.cloudQuality = value, opts -> opts.quality.cloudQuality) + .setImpact(OptionImpact.LOW) + .build()) + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("soundCategory.weather")) + .setTooltip(I18n.format("sodium.options.weather_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.weatherQuality = value, opts -> opts.quality.weatherQuality) + .setImpact(OptionImpact.MEDIUM) + .build()) + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("sodium.options.leaves_quality.name")) + .setTooltip(I18n.format("sodium.options.leaves_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.leavesQuality = value, opts -> opts.quality.leavesQuality) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(ParticleMode.class, vanillaOpts) + .setName(I18n.format("options.particles")) + .setTooltip(I18n.format("sodium.options.particle_quality.tooltip")) + .setControl(opt -> new CyclingControl<>(opt, ParticleMode.class)) + .setBinding((opts, value) -> opts.particleSetting = value.ordinal(), (opts) -> ParticleMode.fromOrdinal(opts.particleSetting)) + .setImpact(OptionImpact.LOW) + .build()) + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("sodium.options.grass_quality.name")) + .setTooltip(I18n.format("sodium.options.grass_quality.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.grassQuality = value, opts -> opts.quality.grassQuality) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(LightingQuality.class, vanillaOpts) + .setName(I18n.format("options.ao")) + .setTooltip(I18n.format("sodium.options.smooth_lighting.tooltip")) + .setControl(option -> new CyclingControl<>(option, LightingQuality.class)) + .setBinding((opts, value) -> opts.ambientOcclusion = value.getVanilla(), opts -> LightingQuality.fromOrdinal(opts.ambientOcclusion)) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + // TODO + /*.add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(new TranslatableText("options.biomeBlendRadius")) + .setTooltip(new TranslatableText("sodium.options.biome_blend.tooltip")) + .setControl(option -> new SliderControl(option, 0, 7, 1, ControlValueFormatter.quantityOrDisabled("sodium.options.biome_blend.value", "gui.none"))) + .setBinding((opts, value) -> opts.biomeBlendRadius = value, opts -> opts.biomeBlendRadius) + .setImpact(OptionImpact.LOW) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(new TranslatableText("options.entityDistanceScaling")) + .setTooltip(new TranslatableText("sodium.options.entity_distance.tooltip")) + .setControl(option -> new SliderControl(option, 50, 500, 25, ControlValueFormatter.percentage())) + .setBinding((opts, value) -> opts.entityDistanceScaling = value / 100.0F, opts -> Math.round(opts.entityDistanceScaling * 100.0F)) + .setImpact(OptionImpact.MEDIUM) + .build() + )*/ + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("options.entityShadows")) + .setTooltip(I18n.format("sodium.options.entity_shadows.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.entityShadows = value, opts -> opts.quality.entityShadows) + .setImpact(OptionImpact.LOW) + .build()) + .add(OptionImpl.createBuilder(GraphicsQuality.class, sodiumOpts) + .setName(I18n.format("sodium.options.vignette.name")) + .setTooltip(I18n.format("sodium.options.vignette.tooltip")) + .setControl(option -> new CyclingControl<>(option, GraphicsQuality.class)) + .setBinding((opts, value) -> opts.quality.enableVignette = value, opts -> opts.quality.enableVignette) + .setImpact(OptionImpact.LOW) + .build()) + .add(Settings.TOTAL_STARS.option) + .build()); + + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(I18n.format("options.mipmapLevels")) + .setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip")) + .setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier())) + .setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_ASSET_RELOAD) + .build()) + .build()); + groups.add(OptionGroup.createBuilder() + .add(Settings.MODE_GLINT_INV.option) + .add(Settings.MODE_GLINT_WORLD.option) + .build()); + + return new OptionPage(I18n.format("sodium.options.pages.quality"), ImmutableList.copyOf(groups)); + } + + public static OptionPage advanced() { + final List groups = new ArrayList<>(); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_chunk_multidraw.name")) + .setTooltip(I18n.format("sodium.options.use_chunk_multidraw.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advanced.useChunkMultidraw = value, opts -> opts.advanced.useChunkMultidraw) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .setImpact(OptionImpact.EXTREME) + .setEnabled(MultidrawChunkRenderBackend.isSupported(sodiumOpts.getData().advanced.ignoreDriverBlacklist)) + .build()) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_vertex_objects.name")) + .setTooltip(I18n.format("sodium.options.use_vertex_objects.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advanced.useVertexArrayObjects = value, opts -> opts.advanced.useVertexArrayObjects) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .setImpact(OptionImpact.LOW) + .build()) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_block_face_culling.name")) + .setTooltip(I18n.format("sodium.options.use_block_face_culling.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.MEDIUM) + .setBinding((opts, value) -> opts.advanced.useBlockFaceCulling = value, opts -> opts.advanced.useBlockFaceCulling) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_compact_vertex_format.name")) + .setTooltip(I18n.format("sodium.options.use_compact_vertex_format.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.MEDIUM) + .setBinding((opts, value) -> opts.advanced.useCompactVertexFormat = value, opts -> opts.advanced.useCompactVertexFormat) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_fog_occlusion.name")) + .setTooltip(I18n.format("sodium.options.use_fog_occlusion.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advanced.useFogOcclusion = value, opts -> opts.advanced.useFogOcclusion) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.translucency_sorting.name")) + .setTooltip(I18n.format("sodium.options.translucency_sorting.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advanced.translucencySorting = value, opts -> opts.advanced.translucencySorting) + .setImpact(OptionImpact.MEDIUM) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_entity_culling.name")) + .setTooltip(I18n.format("sodium.options.use_entity_culling.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.MEDIUM) + .setBinding((opts, value) -> opts.advanced.useEntityCulling = value, opts -> opts.advanced.useEntityCulling) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_particle_culling.name")) + .setTooltip(I18n.format("sodium.options.use_particle_culling.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.MEDIUM) + .setBinding((opts, value) -> opts.advanced.useParticleCulling = value, opts -> opts.advanced.useParticleCulling) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.animate_only_visible_textures.name")) + .setTooltip(I18n.format("sodium.options.animate_only_visible_textures.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.MEDIUM) + .setBinding((opts, value) -> opts.advanced.animateOnlyVisibleTextures = value, opts -> opts.advanced.animateOnlyVisibleTextures) + .build() + ) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.allow_direct_memory_access.name")) + .setTooltip(I18n.format("sodium.options.allow_direct_memory_access.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.HIGH) + .setBinding((opts, value) -> opts.advanced.allowDirectMemoryAccess = value, opts -> opts.advanced.allowDirectMemoryAccess) + .build() + ) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.ignore_driver_blacklist.name")) + .setTooltip(I18n.format("sodium.options.ignore_driver_blacklist.tooltip")) + .setControl(TickBoxControl::new) + .setBinding((opts, value) -> opts.advanced.ignoreDriverBlacklist = value, opts -> opts.advanced.ignoreDriverBlacklist) + .build() + ) + .build()); + + groups.add(OptionGroup.createBuilder() + .add(Settings.MODE_GUI_BACKGROUND.option) + .add(Settings.GUI_BACKGROUND.option) + .build()); + + return new OptionPage(I18n.format("sodium.options.pages.advanced"), ImmutableList.copyOf(groups)); + } + + public static OptionPage performance() { + final List groups = new ArrayList<>(); + + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(int.class, sodiumOpts) + .setName(I18n.format("sodium.options.chunk_update_threads.name")) + .setTooltip(I18n.format("sodium.options.chunk_update_threads.tooltip")) + .setControl(o -> new SliderControl(o, 0, Runtime.getRuntime().availableProcessors(), 1, ControlValueFormatter.quantityOrDisabled("sodium.options.threads.value", "sodium.options.default"))) + .setImpact(OptionImpact.HIGH) + .setBinding((opts, value) -> opts.performance.chunkBuilderThreads = value, opts -> opts.performance.chunkBuilderThreads) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build() + ) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.always_defer_chunk_updates.name")) + .setTooltip(I18n.format("sodium.options.always_defer_chunk_updates.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.HIGH) + .setBinding((opts, value) -> opts.performance.alwaysDeferChunkUpdates = value, opts -> opts.performance.alwaysDeferChunkUpdates) + .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) + .build()) + .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) + .setName(I18n.format("sodium.options.use_no_error_context.name")) + .setTooltip(I18n.format("sodium.options.use_no_error_context.tooltip")) + .setControl(TickBoxControl::new) + .setImpact(OptionImpact.LOW) + .setBinding((opts, value) -> opts.performance.useNoErrorGLContext = value, opts -> opts.performance.useNoErrorGLContext) + .setFlags(OptionFlag.REQUIRES_GAME_RESTART) + .build()) + .build()); + + return new OptionPage(I18n.format("sodium.options.pages.performance"), ImmutableList.copyOf(groups)); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java new file mode 100644 index 000000000..dc1f99488 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java @@ -0,0 +1,127 @@ +package me.jellysquid.mods.sodium.client.gui; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality; +import net.coderbot.iris.Iris; + +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class SodiumGameOptions { + public final QualitySettings quality = new QualitySettings(); + public final AdvancedSettings advanced = new AdvancedSettings(); + public final PerformanceSettings performance = new PerformanceSettings(); + public final NotificationSettings notifications = new NotificationSettings(); + + private Path configPath; + + public static class AdvancedSettings { + public boolean useVertexArrayObjects = true; + public boolean useChunkMultidraw = true; + + public boolean animateOnlyVisibleTextures = true; + public boolean useEntityCulling = true; + public boolean useParticleCulling = true; + public boolean useFogOcclusion = true; + public boolean useCompactVertexFormat = true; + public boolean useBlockFaceCulling = true; + public boolean allowDirectMemoryAccess = true; + public boolean ignoreDriverBlacklist = false; + public boolean translucencySorting = true; + } + + public static class PerformanceSettings { + public int chunkBuilderThreads = 0; + public boolean alwaysDeferChunkUpdates = false; + public boolean useNoErrorGLContext = true; + } + + public static class EntityRenderDistance { + public static double entityRenderDistanceMultiplier = 1.0; + + public static double getRenderDistanceMult() { + return entityRenderDistanceMultiplier; + } + public static void setRenderDistanceMult(double value) { + entityRenderDistanceMultiplier = value; + } + } + public static class QualitySettings { + public GraphicsQuality cloudQuality = GraphicsQuality.DEFAULT; + public GraphicsQuality weatherQuality = GraphicsQuality.DEFAULT; + public GraphicsQuality leavesQuality = GraphicsQuality.DEFAULT; + public GraphicsQuality grassQuality = GraphicsQuality.DEFAULT; + public GraphicsQuality entityShadows = GraphicsQuality.DEFAULT; + public GraphicsQuality enableVignette = GraphicsQuality.DEFAULT; + } + + public static class NotificationSettings { + public boolean hideDonationButton = false; + } + + private static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .setPrettyPrinting() + .excludeFieldsWithModifiers(Modifier.PRIVATE) + .create(); + + public static SodiumGameOptions load(Path path) { + SodiumGameOptions config; + boolean resaveConfig = true; + + if (Files.exists(path)) { + try (FileReader reader = new FileReader(path.toFile())) { + config = GSON.fromJson(reader, SodiumGameOptions.class); + } catch (IOException e) { + throw new RuntimeException("Could not parse config", e); + } catch (JsonSyntaxException e) { + SodiumClientMod.logger().error("Could not parse config, will fallback to default settings", e); + config = new SodiumGameOptions(); + resaveConfig = false; + } + } else { + config = new SodiumGameOptions(); + } + + config.configPath = path; + + try { + if(resaveConfig) + config.writeChanges(); + } catch (IOException e) { + throw new RuntimeException("Couldn't update config file", e); + } + + return config; + } + + public void writeChanges() throws IOException { + final Path dir = this.configPath.getParent(); + + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } else if (!Files.isDirectory(dir)) { + throw new IOException("Not a directory: " + dir); + } + + Files.write(this.configPath, GSON.toJson(this).getBytes(StandardCharsets.UTF_8)); + if(AngelicaConfig.enableIris) { + try { + if (Iris.getIrisConfig() != null) { + Iris.getIrisConfig().save(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java new file mode 100644 index 000000000..30b4ff131 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java @@ -0,0 +1,348 @@ +package me.jellysquid.mods.sodium.client.gui; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import jss.notfine.gui.GuiCustomMenu; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; +import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; +import me.jellysquid.mods.sodium.client.gui.options.OptionImpact; +import me.jellysquid.mods.sodium.client.gui.options.OptionPage; +import me.jellysquid.mods.sodium.client.gui.options.control.Control; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.gui.options.control.element.SodiumControlElementFactory; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; +import me.jellysquid.mods.sodium.client.gui.utils.Drawable; +import me.jellysquid.mods.sodium.client.gui.utils.Element; +import me.jellysquid.mods.sodium.client.gui.utils.URLUtils; +import me.jellysquid.mods.sodium.client.gui.widgets.FlatButtonWidget; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; +import org.lwjgl.input.Keyboard; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; + +public class SodiumOptionsGUI extends GuiScreen { + + private static final SodiumControlElementFactory elementFactory = new SodiumControlElementFactory(); + + private final List children = new CopyOnWriteArrayList<>(); + + private final List pages = new ArrayList<>(); + + private final List> controls = new ArrayList<>(); + private final List drawable = new ArrayList<>(); + + public final GuiScreen prevScreen; + + private OptionPage currentPage; + + private FlatButtonWidget applyButton, closeButton, undoButton; + private FlatButtonWidget donateButton, hideDonateButton; + + private boolean hasPendingChanges; + private ControlElement hoveredElement; + + private OptionPage shaderPacks; + + public SodiumOptionsGUI(GuiScreen prevScreen) { + this.prevScreen = prevScreen; + + this.pages.add(SodiumGameOptionPages.general()); + this.pages.add(SodiumGameOptionPages.quality()); + this.pages.add(SodiumGameOptionPages.advanced()); + this.pages.add(SodiumGameOptionPages.performance()); + + if(AngelicaConfig.enableIris) { + shaderPacks = new OptionPage(I18n.format("options.iris.shaderPackSelection"), ImmutableList.of()); + this.pages.add(shaderPacks); + } + } + + public void setPage(OptionPage page) { + if (AngelicaConfig.enableIris && page == shaderPacks) { + mc.displayGuiScreen(new ShaderPackScreen(this)); + return; + } + + this.currentPage = page; + + this.rebuildGUI(); + } + + @Override + public void initGui() { + this.rebuildGUI(); + } + + private void rebuildGUI() { + this.controls.clear(); + this.children.clear(); + this.drawable.clear(); + + if (this.currentPage == null) { + if (this.pages.isEmpty()) { + throw new IllegalStateException("No pages are available?!"); + } + + // Just use the first page for now + this.currentPage = this.pages.get(0); + } + + this.rebuildGUIPages(); + this.rebuildGUIOptions(); + + this.undoButton = new FlatButtonWidget(new Dim2i(this.width - 211, this.height - 26, 65, 20), I18n.format("sodium.options.buttons.undo"), this::undoChanges); + this.applyButton = new FlatButtonWidget(new Dim2i(this.width - 142, this.height - 26, 65, 20), I18n.format("sodium.options.buttons.apply"), this::applyChanges); + this.closeButton = new FlatButtonWidget(new Dim2i(this.width - 73, this.height - 26, 65, 20), I18n.format("gui.done"), this::onClose); + String donateToJelly = I18n.format("sodium.options.buttons.donate"); + int width = 12 + this.fontRendererObj.getStringWidth(donateToJelly); + this.donateButton = new FlatButtonWidget(new Dim2i(this.width - width - 32, 6, width, 20), donateToJelly, () -> openDonationPage("https://caffeinemc.net/donate")); + this.hideDonateButton = new FlatButtonWidget(new Dim2i(this.width - 26, 6, 20, 20), "x", this::hideDonationButton); + + if (SodiumClientMod.options().notifications.hideDonationButton) { + this.setDonationButtonVisibility(false); + } + + this.children.add(this.undoButton); + this.children.add(this.applyButton); + this.children.add(this.closeButton); + this.children.add(this.donateButton); + this.children.add(this.hideDonateButton); + + for (Element element : this.children) { + if (element instanceof Drawable) { + this.drawable.add((Drawable) element); + } + } + } + + private void setDonationButtonVisibility(boolean value) { + this.donateButton.setVisible(value); + this.hideDonateButton.setVisible(value); + } + + private void hideDonationButton() { + SodiumGameOptions options = SodiumClientMod.options(); + options.notifications.hideDonationButton = true; + + try { + options.writeChanges(); + } catch (IOException e) { + throw new RuntimeException("Failed to save configuration", e); + } + + this.setDonationButtonVisibility(false); + } + + private void rebuildGUIPages() { + int x = 6; + int y = 6; + + for (OptionPage page : this.pages) { + int width = 12 + this.fontRendererObj.getStringWidth(page.getName()); + + FlatButtonWidget button = new FlatButtonWidget(new Dim2i(x, y, width, 18), page.getName(), () -> this.setPage(page)); + button.setSelected(this.currentPage == page); + + x += width + 6; + + this.children.add(button); + } + } + + private void rebuildGUIOptions() { + int x = 6; + int y = 28; + + for (OptionGroup group : this.currentPage.getGroups()) { + // Add each option's control element + for (Option option : group.getOptions()) { + Control control = option.getControl(); + ControlElement element = control.createElement(new Dim2i(x, y, 200, 18), elementFactory); + + this.controls.add(element); + //Safe if SodiumControlElementFactory or a compatible ControlElementFactory is used to create element. + this.children.add((Element)element); + + // Move down to the next option + y += 18; + } + + // Add padding beneath each option group + y += 4; + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float delta) { + super.drawDefaultBackground(); + + this.updateControls(); + + for (Drawable drawable : this.drawable) { + drawable.render(mouseX, mouseY, delta); + } + + if (this.hoveredElement != null) { + this.renderOptionTooltip(this.hoveredElement); + } + } + + private void updateControls() { + ControlElement hovered = this.getActiveControls() + .filter(ControlElement::isHovered) + .findFirst() + .orElse(null); + + boolean hasChanges = this.getAllOptions() + .anyMatch(Option::hasChanged); + + for (OptionPage page : this.pages) { + for (Option option : page.getOptions()) { + if (option.hasChanged()) { + hasChanges = true; + } + } + } + + this.applyButton.setEnabled(hasChanges); + this.undoButton.setVisible(hasChanges); + this.closeButton.setEnabled(!hasChanges); + + this.hasPendingChanges = hasChanges; + this.hoveredElement = hovered; + } + + private Stream> getAllOptions() { + return this.pages.stream() + .flatMap(s -> s.getOptions().stream()); + } + + private Stream> getActiveControls() { + return this.controls.stream(); + } + + private void renderOptionTooltip(ControlElement element) { + Dim2i dim = element.getDimensions(); + + int textPadding = 3; + int boxPadding = 3; + + int boxWidth = 200; + + int boxY = dim.getOriginY(); + int boxX = dim.getLimitX() + boxPadding; + + Option option = element.getOption(); + List tooltip = new ArrayList<>(this.fontRendererObj.listFormattedStringToWidth(option.getTooltip(), boxWidth - (textPadding * 2))); + + OptionImpact impact = option.getImpact(); + + if (impact != null) { + tooltip.add(EnumChatFormatting.GRAY + I18n.format("sodium.options.performance_impact_string", impact.toDisplayString())); + } + + int boxHeight = (tooltip.size() * 12) + boxPadding; + int boxYLimit = boxY + boxHeight; + int boxYCutoff = this.height - 40; + + // If the box is going to be cutoff on the Y-axis, move it back up the difference + if (boxYLimit > boxYCutoff) { + boxY -= boxYLimit - boxYCutoff; + } + + this.drawGradientRect(boxX, boxY, boxX + boxWidth, boxY + boxHeight, 0xE0000000, 0xE0000000); + + for (int i = 0; i < tooltip.size(); i++) { + this.fontRendererObj.drawString(tooltip.get(i), boxX + textPadding, boxY + textPadding + (i * 12), 0xFFFFFFFF); + } + } + + private void applyChanges() { + final HashSet> dirtyStorages = new HashSet<>(); + final EnumSet flags = EnumSet.noneOf(OptionFlag.class); + + this.getAllOptions().forEach((option -> { + if (!option.hasChanged()) { + return; + } + + option.applyChanges(); + + flags.addAll(option.getFlags()); + dirtyStorages.add(option.getStorage()); + })); + + if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) { + this.mc.renderGlobal.loadRenderers(); + } + + if (flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) { + this.mc.getTextureMapBlocks().setMipmapLevels(this.mc.gameSettings.mipmapLevels); + this.mc.refreshResources(); + } + + for (OptionStorage storage : dirtyStorages) { + storage.save(); + } + } + + private void undoChanges() { + this.getAllOptions() + .forEach(Option::reset); + } + + private void openDonationPage(String url) { + URLUtils.open(url); + } + + @Override + public void keyTyped(char typedChar, int keyCode) { + if(keyCode == Keyboard.KEY_ESCAPE && !shouldCloseOnEsc()) { + return; + } else if (keyCode == Keyboard.KEY_ESCAPE) { + onClose(); + return; + } + + if (keyCode == Keyboard.KEY_P && isShiftKeyDown()) { + this.mc.displayGuiScreen(new GuiCustomMenu(this.prevScreen, SodiumGameOptionPages.general(), + SodiumGameOptionPages.quality(), SodiumGameOptionPages.advanced(), SodiumGameOptionPages.performance())); + } + } + + public boolean shouldCloseOnEsc() { + return !this.hasPendingChanges; + } + + // We can't override onGuiClosed due to StackOverflow + public void onClose() { + this.mc.displayGuiScreen(this.prevScreen); + super.onGuiClosed(); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { + super.mouseClicked(mouseX, mouseY, mouseButton); + + this.children.forEach(element -> element.mouseClicked(mouseX, mouseY, mouseButton)); + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { + super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); + + this.children.forEach(element -> element.mouseDragged(mouseX, mouseY)); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/Option.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/Option.java new file mode 100644 index 000000000..8169fff1f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/Option.java @@ -0,0 +1,32 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +import me.jellysquid.mods.sodium.client.gui.options.control.Control; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; + +import java.util.Collection; + +public interface Option { + String getName(); + + String getTooltip(); + + OptionImpact getImpact(); + + Control getControl(); + + T getValue(); + + void setValue(T value); + + void reset(); + + OptionStorage getStorage(); + + boolean isAvailable(); + + boolean hasChanged(); + + void applyChanges(); + + Collection getFlags(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionFlag.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionFlag.java new file mode 100644 index 000000000..d7ac00621 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionFlag.java @@ -0,0 +1,7 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +public enum OptionFlag { + REQUIRES_RENDERER_RELOAD, + REQUIRES_ASSET_RELOAD, + REQUIRES_GAME_RESTART +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionGroup.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionGroup.java new file mode 100644 index 000000000..97426724a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionGroup.java @@ -0,0 +1,39 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.List; + +public class OptionGroup { + private final ImmutableList> options; + + private OptionGroup(ImmutableList> options) { + this.options = options; + } + + public static Builder createBuilder() { + return new Builder(); + } + + public ImmutableList> getOptions() { + return this.options; + } + + public static class Builder { + private final List> options = new ArrayList<>(); + + public Builder add(Option option) { + this.options.add(option); + + return this; + } + + public OptionGroup build() { + Validate.notEmpty(this.options, "At least one option must be specified"); + + return new OptionGroup(ImmutableList.copyOf(this.options)); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpact.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpact.java new file mode 100644 index 000000000..0f6aff1ad --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpact.java @@ -0,0 +1,24 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; + +public enum OptionImpact { + LOW(EnumChatFormatting.GREEN, I18n.format("sodium.option_impact.low")), + MEDIUM(EnumChatFormatting.YELLOW, I18n.format("sodium.option_impact.medium")), + HIGH(EnumChatFormatting.GOLD, I18n.format("sodium.option_impact.high")), + EXTREME(EnumChatFormatting.RED, I18n.format("sodium.option_impact.extreme")), + VARIES(EnumChatFormatting.WHITE, I18n.format("sodium.option_impact.varies")); + + private final EnumChatFormatting color; + private final String text; + + OptionImpact(EnumChatFormatting color, String text) { + this.color = color; + this.text = text; + } + + public String toDisplayString() { + return this.color + this.text; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpl.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpl.java new file mode 100644 index 000000000..fa85b6848 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionImpl.java @@ -0,0 +1,210 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +import me.jellysquid.mods.sodium.client.gui.options.binding.GenericBinding; +import me.jellysquid.mods.sodium.client.gui.options.binding.OptionBinding; +import me.jellysquid.mods.sodium.client.gui.options.control.Control; +import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +public class OptionImpl implements Option { + private final OptionStorage storage; + + private final OptionBinding binding; + private final Control control; + + private final EnumSet flags; + + private final String name; + private final String tooltip; + + private final OptionImpact impact; + + private T value; + private T modifiedValue; + + private BooleanSupplier iris$dynamicallyEnabled; + private final boolean enabled; + + private OptionImpl(OptionStorage storage, + String name, + String tooltip, + OptionBinding binding, + Function, Control> control, + EnumSet flags, + OptionImpact impact, + boolean enabled) { + this.storage = storage; + this.name = name; + this.tooltip = tooltip; + this.binding = binding; + this.impact = impact; + this.flags = flags; + this.control = control.apply(this); + this.enabled = enabled; + + this.reset(); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getTooltip() { + return this.tooltip; + } + + @Override + public OptionImpact getImpact() { + return this.impact; + } + + @Override + public Control getControl() { + return this.control; + } + + @Override + public T getValue() { + return this.modifiedValue; + } + + @Override + public void setValue(T value) { + this.modifiedValue = value; + } + + @Override + public void reset() { + this.value = this.binding.getValue(this.storage.getData()); + this.modifiedValue = this.value; + } + + @Override + public OptionStorage getStorage() { + return this.storage; + } + + @Override + public boolean isAvailable() { + if (iris$dynamicallyEnabled != null) { + return iris$dynamicallyEnabled.getAsBoolean(); + } + return this.enabled; + } + + @Override + public boolean hasChanged() { + return !this.value.equals(this.modifiedValue); + } + + @Override + public void applyChanges() { + this.binding.setValue(this.storage.getData(), this.modifiedValue); + this.value = this.modifiedValue; + } + public void iris$dynamicallyEnable(BooleanSupplier enabled) { + this.iris$dynamicallyEnabled = enabled; + } + + @Override + public Collection getFlags() { + return this.flags; + } + + public static OptionImpl.Builder createBuilder(Class type, OptionStorage storage) { + return new Builder<>(storage); + } + + public static class Builder { + private final OptionStorage storage; + private String name; + private String tooltip; + private OptionBinding binding; + private Function, Control> control; + private OptionImpact impact; + private final EnumSet flags = EnumSet.noneOf(OptionFlag.class); + private boolean enabled = true; + + private Builder(OptionStorage storage) { + this.storage = storage; + } + + public Builder setName(String name) { + Validate.notNull(name, "Argument must not be null"); + + this.name = name; + + return this; + } + + public Builder setTooltip(String tooltip) { + Validate.notNull(tooltip, "Argument must not be null"); + + this.tooltip = tooltip; + + return this; + } + + public Builder setBinding(BiConsumer setter, Function getter) { + Validate.notNull(setter, "Setter must not be null"); + Validate.notNull(getter, "Getter must not be null"); + + this.binding = new GenericBinding<>(setter, getter); + + return this; + } + + + public Builder setBinding(OptionBinding binding) { + Validate.notNull(binding, "Argument must not be null"); + + this.binding = binding; + + return this; + } + + public Builder setControl(Function, Control> control) { + Validate.notNull(control, "Argument must not be null"); + + this.control = control; + + return this; + } + + public Builder setImpact(OptionImpact impact) { + this.impact = impact; + + return this; + } + + public Builder setEnabled(boolean value) { + this.enabled = value; + + return this; + } + + public Builder setFlags(OptionFlag... flags) { + Collections.addAll(this.flags, flags); + + return this; + } + + public OptionImpl build() { + Validate.notNull(this.name, "Name must be specified"); + Validate.notNull(this.tooltip, "Tooltip must be specified"); + Validate.notNull(this.binding, "Option binding must be specified"); + Validate.notNull(this.control, "Control must be specified"); + + return new OptionImpl<>(this.storage, this.name, this.tooltip, this.binding, this.control, this.flags, this.impact, this.enabled); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionPage.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionPage.java new file mode 100644 index 000000000..a02254c90 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/OptionPage.java @@ -0,0 +1,35 @@ +package me.jellysquid.mods.sodium.client.gui.options; + +import com.google.common.collect.ImmutableList; + +public class OptionPage { + private final String name; + private final ImmutableList groups; + private final ImmutableList> options; + + public OptionPage(String name, ImmutableList groups) { + this.name = name; + this.groups = groups; + + ImmutableList.Builder> builder = ImmutableList.builder(); + + for (OptionGroup group : groups) { + builder.addAll(group.getOptions()); + } + + this.options = builder.build(); + } + + public ImmutableList getGroups() { + return this.groups; + } + + public ImmutableList> getOptions() { + return this.options; + } + + public String getName() { + return this.name; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/GenericBinding.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/GenericBinding.java new file mode 100644 index 000000000..9e166c93e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/GenericBinding.java @@ -0,0 +1,24 @@ +package me.jellysquid.mods.sodium.client.gui.options.binding; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class GenericBinding implements OptionBinding { + private final BiConsumer setter; + private final Function getter; + + public GenericBinding(BiConsumer setter, Function getter) { + this.setter = setter; + this.getter = getter; + } + + @Override + public void setValue(S storage, T value) { + this.setter.accept(storage, value); + } + + @Override + public T getValue(S storage) { + return this.getter.apply(storage); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/OptionBinding.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/OptionBinding.java new file mode 100644 index 000000000..5e597a679 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/binding/OptionBinding.java @@ -0,0 +1,7 @@ +package me.jellysquid.mods.sodium.client.gui.options.binding; + +public interface OptionBinding { + void setValue(S storage, T value); + + T getValue(S storage); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/Control.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/Control.java new file mode 100644 index 000000000..509f27a5f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/Control.java @@ -0,0 +1,13 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public interface Control { + Option getOption(); + + ControlElement createElement(Dim2i dim, ControlElementFactory factory); + + int getMaxWidth(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlElement.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlElement.java new file mode 100644 index 000000000..b8a09e6fd --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlElement.java @@ -0,0 +1,11 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public interface ControlElement { + public boolean isHovered(); + public Option getOption(); + public Dim2i getDimensions(); + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlValueFormatter.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlValueFormatter.java new file mode 100644 index 000000000..69c454848 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/ControlValueFormatter.java @@ -0,0 +1,47 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import net.minecraft.client.resources.I18n; + +public interface ControlValueFormatter { + static ControlValueFormatter guiScale() { + return (v) -> (v == 0) ? I18n.format("options.guiScale.auto") : I18n.format(v + "x"); + } + + static ControlValueFormatter fpsLimit() { + return (v) -> (v == 260) ? I18n.format("options.framerateLimit.max") : I18n.format("options.framerate", v); + } + + static ControlValueFormatter brightness() { + return (v) -> { + if (v == 0) { + return I18n.format("options.gamma.min"); + } else if (v == 100) { + return I18n.format("options.gamma.max"); + } else { + return v + "%"; + } + }; + } + + String format(int value); + + static ControlValueFormatter percentage() { + return (v) -> v + "%"; + } + + static ControlValueFormatter multiplier() { + return (v) -> v + "x"; + } + + static ControlValueFormatter quantity(String name) { + return (v) -> I18n.format(name, v); + } + + static ControlValueFormatter quantityOrDisabled(String name, String disableText) { + return (v) -> I18n.format(v == 0 ? disableText : name, v); + } + + static ControlValueFormatter number() { + return String::valueOf; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/CyclingControl.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/CyclingControl.java new file mode 100644 index 000000000..3b87e1e1d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/CyclingControl.java @@ -0,0 +1,62 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory; +import me.jellysquid.mods.sodium.client.gui.options.named.NamedState; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.client.resources.I18n; +import org.apache.commons.lang3.Validate; + +public class CyclingControl> implements Control { + private final Option option; + private final T[] allowedValues; + private final String[] names; + + public CyclingControl(Option option, Class enumType) { + this(option, enumType, enumType.getEnumConstants()); + } + + public CyclingControl(Option option, Class enumType, String[] names) { + T[] universe = enumType.getEnumConstants(); + + Validate.isTrue(universe.length == names.length, "Mismatch between universe length and names array length"); + Validate.notEmpty(universe, "The enum universe must contain at least one item"); + + this.option = option; + this.allowedValues = universe; + this.names = names; + } + + public CyclingControl(Option option, Class enumType, T[] allowedValues) { + T[] universe = enumType.getEnumConstants(); + + this.option = option; + this.allowedValues = allowedValues; + this.names = new String[universe.length]; + + for (int i = 0; i < this.names.length; i++) { + String name; + T value = universe[i]; + + name = I18n.format(value instanceof NamedState namedState ? namedState.getKey() : value.name()); + + this.names[i] = name; + } + } + + @Override + public Option getOption() { + return this.option; + } + + @Override + public ControlElement createElement(Dim2i dim, ControlElementFactory factory) { + return factory.cyclingControlElement(this.option, dim, this.allowedValues, this.names); + } + + @Override + public int getMaxWidth() { + return 70; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/SliderControl.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/SliderControl.java new file mode 100644 index 000000000..05e71dc51 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/SliderControl.java @@ -0,0 +1,43 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import org.apache.commons.lang3.Validate; + +public class SliderControl implements Control { + private final Option option; + + private final int min, max, interval; + + private final ControlValueFormatter mode; + + public SliderControl(Option option, int min, int max, int interval, ControlValueFormatter mode) { + Validate.isTrue(max > min, "The maximum value must be greater than the minimum value"); + Validate.isTrue(interval > 0, "The slider interval must be greater than zero"); + Validate.isTrue(((max - min) % interval) == 0, "The maximum value must be divisable by the interval"); + Validate.notNull(mode, "The slider mode must not be null"); + + this.option = option; + this.min = min; + this.max = max; + this.interval = interval; + this.mode = mode; + } + + @Override + public ControlElement createElement(Dim2i dim, ControlElementFactory factory) { + return factory.sliderControlElement(this.option, dim, this.min, this.max, this.interval, this.mode); + } + + @Override + public Option getOption() { + return this.option; + } + + @Override + public int getMaxWidth() { + return 130; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/TickBoxControl.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/TickBoxControl.java new file mode 100644 index 000000000..ba5783609 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/TickBoxControl.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.gui.options.control; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public class TickBoxControl implements Control { + private final Option option; + + public TickBoxControl(Option option) { + this.option = option; + } + + @Override + public ControlElement createElement(Dim2i dim, ControlElementFactory factory) { + return factory.tickBoxElement(this.option, dim); + } + + @Override + public int getMaxWidth() { + return 30; + } + + @Override + public Option getOption() { + return this.option; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/ControlElementFactory.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/ControlElementFactory.java new file mode 100644 index 000000000..75517192e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/ControlElementFactory.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.client.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public interface ControlElementFactory { + + > ControlElement cyclingControlElement(Option option, Dim2i dim, T[] allowedValues, String[] names); + + ControlElement sliderControlElement(Option option, Dim2i dim, int min, int max, int interval, ControlValueFormatter formatter); + + ControlElement tickBoxElement(Option option, Dim2i dim); + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElement.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElement.java new file mode 100644 index 000000000..8cc8dc905 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElement.java @@ -0,0 +1,60 @@ +package me.jellysquid.mods.sodium.client.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.gui.widgets.AbstractWidget; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.util.EnumChatFormatting; + +public class SodiumControlElement extends AbstractWidget implements ControlElement { + protected final Option option; + protected final Dim2i dim; + protected boolean hovered; + + public SodiumControlElement(Option option, Dim2i dim) { + this.option = option; + this.dim = dim; + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + String name = this.option.getName(); + String label; + + if (this.hovered && this.font.getStringWidth(name) > (this.dim.getWidth() - this.option.getControl().getMaxWidth())) { + name = name.substring(0, Math.min(name.length(), 10)) + "..."; + } + + if (this.option.isAvailable()) { + if (this.option.hasChanged()) { + label = EnumChatFormatting.ITALIC + name + " *"; + } else { + label = EnumChatFormatting.WHITE + name; + } + } else { + label = String.valueOf(EnumChatFormatting.GRAY) + EnumChatFormatting.STRIKETHROUGH + name; + } + + this.hovered = this.dim.containsCursor(mouseX, mouseY); + + + this.drawRect(this.dim.getOriginX(), this.dim.getOriginY(), this.dim.getLimitX(), this.dim.getLimitY(), this.hovered ? 0xE0000000 : 0x90000000); + this.drawString(label, this.dim.getOriginX() + 6, this.dim.getCenterY() - 4, 0xFFFFFFFF); + } + + @Override + public boolean isHovered() { + return this.hovered; + } + + @Override + public Option getOption() { + return this.option; + } + + @Override + public Dim2i getDimensions() { + return this.dim; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElementFactory.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElementFactory.java new file mode 100644 index 000000000..8cf67f74a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/control/element/SodiumControlElementFactory.java @@ -0,0 +1,253 @@ +package me.jellysquid.mods.sodium.client.gui.options.control.element; + +import me.jellysquid.mods.sodium.client.gui.options.Option; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement; +import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter; +import me.jellysquid.mods.sodium.client.gui.utils.Rect2i; +import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.minecraft.util.MathHelper; + +public class SodiumControlElementFactory implements ControlElementFactory { + @Override + public > ControlElement cyclingControlElement(Option option, Dim2i dim, T[] allowedValues, String[] names) { + return new CyclingControlElement(option, dim, allowedValues, names); + } + + @Override + public ControlElement sliderControlElement(Option option, Dim2i dim, int min, int max, int interval, ControlValueFormatter formatter) { + return new SliderControlElement(option, dim, min, max, interval, formatter); + } + + @Override + public ControlElement tickBoxElement(Option option, Dim2i dim) { + return new TickBoxControlElement(option, dim); + } + + private static class CyclingControlElement> extends SodiumControlElement { + private final T[] allowedValues; + private final String[] names; + private int currentIndex; + + public CyclingControlElement(Option option, Dim2i dim, T[] allowedValues, String[] names) { + super(option, dim); + + this.allowedValues = allowedValues; + this.names = names; + this.currentIndex = 0; + + for (int i = 0; i < allowedValues.length; i++) { + if (allowedValues[i] == option.getValue()) { + this.currentIndex = i; + break; + } + } + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + super.render(mouseX, mouseY, delta); + + Enum value = this.option.getValue(); + String name = this.names[value.ordinal()]; + + int strWidth = this.getTextWidth(name); + this.drawString(name, this.dim.getLimitX() - strWidth - 6, this.dim.getCenterY() - 4, 0xFFFFFFFF); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.option.isAvailable() && button == 0 && this.dim.containsCursor(mouseX, mouseY)) { + this.currentIndex = (this.option.getValue().ordinal() + 1) % this.allowedValues.length; + this.option.setValue(this.allowedValues[this.currentIndex]); + this.playClickSound(); + + return true; + } + + return false; + } + } + + private static class SliderControlElement extends SodiumControlElement { + private static final int THUMB_WIDTH = 2, TRACK_HEIGHT = 1; + + private final Rect2i sliderBounds; + private final ControlValueFormatter formatter; + + private final int min; + private final int range; + private final int interval; + + private double thumbPosition; + + public SliderControlElement(Option option, Dim2i dim, int min, int max, int interval, ControlValueFormatter formatter) { + super(option, dim); + + this.min = min; + this.range = max - min; + this.interval = interval; + this.thumbPosition = this.getThumbPositionForValue(option.getValue()); + this.formatter = formatter; + + this.sliderBounds = new Rect2i(dim.getLimitX() - 96, dim.getCenterY() - 5, 90, 10); + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + super.render(mouseX, mouseY, delta); + + if (this.option.isAvailable() && this.hovered) { + this.renderSlider(); + } else { + this.renderStandaloneValue(); + } + } + + private void renderStandaloneValue() { + int sliderX = this.sliderBounds.getX(); + int sliderY = this.sliderBounds.getY(); + int sliderWidth = this.sliderBounds.getWidth(); + int sliderHeight = this.sliderBounds.getHeight(); + + String label = this.formatter.format(this.option.getValue()); + int labelWidth = this.font.getStringWidth(label); + + this.drawString(label, sliderX + sliderWidth - labelWidth, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); + } + + private void renderSlider() { + int sliderX = this.sliderBounds.getX(); + int sliderY = this.sliderBounds.getY(); + int sliderWidth = this.sliderBounds.getWidth(); + int sliderHeight = this.sliderBounds.getHeight(); + + this.thumbPosition = this.getThumbPositionForValue(option.getValue()); + + double thumbOffset = MathHelper.clamp_double((double) (this.getIntValue() - this.min) / this.range * sliderWidth, 0, sliderWidth); + + double thumbX = sliderX + thumbOffset - THUMB_WIDTH; + double trackY = sliderY + (sliderHeight / 2) - ((double) TRACK_HEIGHT / 2); + + this.drawRect(thumbX, sliderY, thumbX + (THUMB_WIDTH * 2), sliderY + sliderHeight, 0xFFFFFFFF); + this.drawRect(sliderX, trackY, sliderX + sliderWidth, trackY + TRACK_HEIGHT, 0xFFFFFFFF); + + String label = String.valueOf(this.getIntValue()); + + int labelWidth = this.font.getStringWidth(label); + + this.drawString(label, sliderX - labelWidth - 6, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); + } + + public int getIntValue() { + return this.min + (this.interval * (int) Math.round(this.getSnappedThumbPosition() / this.interval)); + } + + public double getSnappedThumbPosition() { + return this.thumbPosition / (1.0D / this.range); + } + + public double getThumbPositionForValue(int value) { + return (value - this.min) * (1.0D / this.range); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.option.isAvailable() && button == 0 && this.sliderBounds.contains((int) mouseX, (int) mouseY)) { + this.setValueFromMouse(mouseX); + + return true; + } + + return false; + } + + private void setValueFromMouse(double d) { + this.setValue((d - (double) this.sliderBounds.getX()) / (double) this.sliderBounds.getWidth()); + } + + private void setValue(double d) { + this.thumbPosition = MathHelper.clamp_double(d, 0.0D, 1.0D); + + int value = this.getIntValue(); + + if (this.option.getValue() != value) { + this.option.setValue(value); + } + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY) { + if (this.option.isAvailable() && this.sliderBounds.contains((int) mouseX, (int) mouseY)) { + this.setValueFromMouse(mouseX); + + return true; + } + + return false; + } + } + + private static class TickBoxControlElement extends SodiumControlElement { + private final Rect2i button; + + public TickBoxControlElement(Option option, Dim2i dim) { + super(option, dim); + + this.button = new Rect2i(dim.getLimitX() - 16, dim.getCenterY() - 5, 10, 10); + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + super.render(mouseX, mouseY, delta); + + final int x = this.button.getX(); + final int y = this.button.getY(); + final int w = x + this.button.getWidth(); + final int h = y + this.button.getHeight(); + + final boolean enabled = this.option.isAvailable(); + final boolean ticked = enabled && this.option.getValue(); + + final int color; + + if (enabled) { + color = ticked ? 0xFF94E4D3 : 0xFFFFFFFF; + } else { + color = 0xFFAAAAAA; + } + + if (ticked) { + this.drawRect(x + 2, y + 2, w - 2, h - 2, color); + } + + this.drawRectOutline(x, y, w, h, color); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.option.isAvailable() && button == 0 && this.dim.containsCursor(mouseX, mouseY)) { + this.option.setValue(!this.option.getValue()); + this.playClickSound(); + + return true; + } + + return false; + } + + protected void drawRectOutline(int x, int y, int w, int h, int color) { + final float a = (float) (color >> 24 & 255) / 255.0F; + final float r = (float) (color >> 16 & 255) / 255.0F; + final float g = (float) (color >> 8 & 255) / 255.0F; + final float b = (float) (color & 255) / 255.0F; + + this.drawQuads(vertices -> { + addQuad(vertices, x, y, w, y + 1, a, r, g, b); + addQuad(vertices, x, h - 1, w, h, a, r, g, b); + addQuad(vertices, x, y, x + 1, h, a, r, g, b); + addQuad(vertices, w - 1, y, w, h, a, r, g, b); + }); + } + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsMode.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsMode.java new file mode 100644 index 000000000..8dac938de --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsMode.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.gui.options.named; + +public enum GraphicsMode implements NamedState { + FANCY("options.graphics.fancy"), + FAST("options.graphics.fast"); + + private final String name; + + GraphicsMode(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + + public boolean isFancy() { + return this == FANCY; + } + + public static GraphicsMode fromBoolean(boolean isFancy) { + return isFancy ? FANCY : FAST; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsQuality.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsQuality.java new file mode 100644 index 000000000..ebe46003c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/GraphicsQuality.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.gui.options.named; + +import net.minecraft.client.Minecraft; + +public enum GraphicsQuality implements NamedState { + DEFAULT("generator.default"), + FANCY("options.graphics.fancy"), + FAST("options.graphics.fast"); + + private final String name; + + GraphicsQuality(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + + public boolean isFancy() { + return this == FANCY || (this == DEFAULT && Minecraft.getMinecraft().gameSettings.fancyGraphics); + } + +} + diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/LightingQuality.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/LightingQuality.java new file mode 100644 index 000000000..dbcbe6321 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/LightingQuality.java @@ -0,0 +1,31 @@ +package me.jellysquid.mods.sodium.client.gui.options.named; + +public enum LightingQuality implements NamedState { + OFF("options.ao.off"), + LOW("options.ao.min"), + HIGH("options.ao.max"); + + private static final LightingQuality[] VALUES = values(); + + private final String name; + + private final int vanilla; + + LightingQuality(String name) { + this.name = name; + this.vanilla = ordinal(); + } + + @Override + public String getKey() { + return this.name; + } + + public int getVanilla() { + return vanilla; + } + + public static LightingQuality fromOrdinal(int ordinal) { + return VALUES[ordinal]; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/NamedState.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/NamedState.java new file mode 100644 index 000000000..b1384b602 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/NamedState.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.gui.options.named; + +public interface NamedState { + String getKey(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/ParticleMode.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/ParticleMode.java new file mode 100644 index 000000000..d22dd679d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/named/ParticleMode.java @@ -0,0 +1,24 @@ +package me.jellysquid.mods.sodium.client.gui.options.named; + +public enum ParticleMode implements NamedState { + ALL("options.particles.all"), + DECREASED("options.particles.decreased"), + MINIMAL("options.particles.minimal"); + + private static final ParticleMode[] VALUES = values(); + + private final String name; + + ParticleMode(String name) { + this.name = name; + } + + @Override + public String getKey() { + return this.name; + } + + public static ParticleMode fromOrdinal(int ordinal) { + return VALUES[ordinal]; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/MinecraftOptionsStorage.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/MinecraftOptionsStorage.java new file mode 100644 index 000000000..1630c57ef --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/MinecraftOptionsStorage.java @@ -0,0 +1,25 @@ +package me.jellysquid.mods.sodium.client.gui.options.storage; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.GameSettings; + +public class MinecraftOptionsStorage implements OptionStorage { + private final Minecraft client; + + public MinecraftOptionsStorage() { + this.client = Minecraft.getMinecraft(); + } + + @Override + public GameSettings getData() { + return this.client.gameSettings; + } + + @Override + public void save() { + this.getData().saveOptions(); + + SodiumClientMod.logger().info("Flushed changes to Minecraft configuration"); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/OptionStorage.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/OptionStorage.java new file mode 100644 index 000000000..a2e4966da --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/OptionStorage.java @@ -0,0 +1,7 @@ +package me.jellysquid.mods.sodium.client.gui.options.storage; + +public interface OptionStorage { + T getData(); + + void save(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/SodiumOptionsStorage.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/SodiumOptionsStorage.java new file mode 100644 index 000000000..c8b3372b5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/options/storage/SodiumOptionsStorage.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.gui.options.storage; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions; + +import java.io.IOException; + +public class SodiumOptionsStorage implements OptionStorage { + private final SodiumGameOptions options; + + public SodiumOptionsStorage() { + this.options = SodiumClientMod.options(); + } + + @Override + public SodiumGameOptions getData() { + return this.options; + } + + @Override + public void save() { + try { + this.options.writeChanges(); + } catch (IOException e) { + throw new RuntimeException("Couldn't save configuration changes", e); + } + + SodiumClientMod.logger().info("Flushed changes to Rubidium configuration"); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Drawable.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Drawable.java new file mode 100644 index 000000000..19cb8a161 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Drawable.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.gui.utils; + +public interface Drawable { + void render(int mouseX, int mouseY, float delta); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Element.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Element.java new file mode 100644 index 000000000..feec11bb5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Element.java @@ -0,0 +1,11 @@ +package me.jellysquid.mods.sodium.client.gui.utils; + +public interface Element { + default boolean mouseClicked(double mouseX, double mouseY, int button) { + return false; + } + + default boolean mouseDragged(double mouseX, double mouseY) { + return false; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Rect2i.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Rect2i.java new file mode 100644 index 000000000..54491198e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/Rect2i.java @@ -0,0 +1,35 @@ +package me.jellysquid.mods.sodium.client.gui.utils; + +public class Rect2i { + private final int x; + private final int y; + private final int width; + private final int height; + + public Rect2i(int i, int j, int k, int l) { + this.x = i; + this.y = j; + this.width = k; + this.height = l; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getWidth() { + return this.width; + } + + public int getHeight() { + return this.height; + } + + public boolean contains(int x, int y) { + return x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + this.height; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/URLUtils.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/URLUtils.java new file mode 100644 index 000000000..9fb35cfda --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/utils/URLUtils.java @@ -0,0 +1,27 @@ +package me.jellysquid.mods.sodium.client.gui.utils; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import net.minecraft.util.Util; + +import java.io.IOException; + +public class URLUtils { + + private static String[] getURLOpenCommand(String url) { + return switch (Util.getOSType()) { + case WINDOWS -> new String[]{"rundll32", "url.dll,FileProtocolHandler", url}; + case OSX -> new String[]{"open", url}; + case UNKNOWN, LINUX, SOLARIS -> new String[]{"xdg-open", url}; + }; + } + + public static void open(String url) { + try { + Runtime.getRuntime().exec(getURLOpenCommand(url)); + } catch (IOException exception) { + SodiumClientMod.logger().error("Couldn't open url '{}'", url, exception); + } + + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/AbstractWidget.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/AbstractWidget.java new file mode 100644 index 000000000..97f9feec4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/AbstractWidget.java @@ -0,0 +1,64 @@ +package me.jellysquid.mods.sodium.client.gui.widgets; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import me.jellysquid.mods.sodium.client.gui.utils.Drawable; +import me.jellysquid.mods.sodium.client.gui.utils.Element; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +import java.util.function.Consumer; + +public abstract class AbstractWidget implements Drawable, Element { + protected final FontRenderer font; + + protected AbstractWidget() { + this.font = Minecraft.getMinecraft().fontRenderer; + } + + protected void drawString(String str, int x, int y, int color) { + this.font.drawString(str, x, y, color); + } + + protected void drawRect(double x1, double y1, double x2, double y2, int color) { + float a = (float) (color >> 24 & 255) / 255.0F; + float r = (float) (color >> 16 & 255) / 255.0F; + float g = (float) (color >> 8 & 255) / 255.0F; + float b = (float) (color & 255) / 255.0F; + + this.drawQuads(vertices -> addQuad(vertices, x1, y1, x2, y2, a, r, g, b)); + } + + protected void drawQuads(Consumer consumer) { + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GLStateManager.defaultBlendFunc(); + + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + consumer.accept(tessellator); + tessellator.draw(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + } + + protected static void addQuad(Tessellator consumer, double x1, double y1, double x2, double y2, float a, float r, float g, float b) { + consumer.setColorRGBA_F(r, g, b, a); + consumer.addVertex(x2, y1, 0.0D); + consumer.addVertex(x1, y1, 0.0D); + consumer.addVertex(x1, y2, 0.0D); + consumer.addVertex(x2, y2, 0.0D); + } + + protected void playClickSound() { + Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); + } + + protected int getTextWidth(String text) { + return this.font.getStringWidth(text); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/FlatButtonWidget.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/FlatButtonWidget.java new file mode 100644 index 000000000..956cb6426 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/widgets/FlatButtonWidget.java @@ -0,0 +1,69 @@ +package me.jellysquid.mods.sodium.client.gui.widgets; + +import me.jellysquid.mods.sodium.client.gui.utils.Drawable; +import me.jellysquid.mods.sodium.client.util.Dim2i; + +public class FlatButtonWidget extends AbstractWidget implements Drawable { + private final Dim2i dim; + private final String label; + private final Runnable action; + + private boolean selected; + private boolean enabled = true; + private boolean visible = true; + + public FlatButtonWidget(Dim2i dim, String label, Runnable action) { + this.dim = dim; + this.label = label; + this.action = action; + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + if (!this.visible) { + return; + } + + boolean hovered = this.dim.containsCursor(mouseX, mouseY); + + int backgroundColor = this.enabled ? (hovered ? 0xE0000000 : 0x90000000) : 0x60000000; + int textColor = this.enabled ? 0xFFFFFFFF : 0x90FFFFFF; + + int strWidth = this.font.getStringWidth(this.label); + + this.drawRect(this.dim.getOriginX(), this.dim.getOriginY(), this.dim.getLimitX(), this.dim.getLimitY(), backgroundColor); + this.drawString(this.label, this.dim.getCenterX() - (strWidth / 2), this.dim.getCenterY() - 4, textColor); + + if (this.enabled && this.selected) { + this.drawRect(this.dim.getOriginX(), this.dim.getLimitY() - 1, this.dim.getLimitX(), this.dim.getLimitY(), 0xFF94E4D3); + } + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.enabled || !this.visible) { + return false; + } + + if (button == 0 && this.dim.containsCursor(mouseX, mouseY)) { + this.action.run(); + this.playClickSound(); + + return true; + } + + return false; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/EntityLighter.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/EntityLighter.java new file mode 100644 index 000000000..6c419bd29 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/EntityLighter.java @@ -0,0 +1,101 @@ +package me.jellysquid.mods.sodium.client.model.light; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import net.minecraft.util.MathHelper; +import net.minecraft.entity.Entity; +import net.minecraft.world.EnumSkyBlock; + +import static org.joml.Math.lerp; + +public class EntityLighter { + private static final double MIN_BOX_SIZE = 0.001D; + + private static final double MAX_LIGHT_VAL = 15.0; + private static final double MAX_LIGHTMAP_COORD = 240.0D; + + public static int getBlendedLight(Entity entity, float tickDelta) { + boolean calcBlockLight = !entity.isBurning(); + + // Find the interpolated position of the entity + double x1 = lerp(entity.prevPosX, entity.posX, tickDelta); + double y1 = lerp(entity.prevPosY, entity.posY, tickDelta); + double z1 = lerp(entity.prevPosZ, entity.posZ, tickDelta); + + // Bounding boxes with no volume cause issues, ensure they're non-zero + // Notably, armor stands in "Marker" mode decide this is a cute thing to do + // https://github.com/jellysquid3/sodium-fabric/issues/60 + double width = Math.max(entity.width, MIN_BOX_SIZE); + double height = Math.max(entity.height, MIN_BOX_SIZE); + + double x2 = x1 + width; + double y2 = y1 + height; + double z2 = z1 + width; + + // The sampling volume of blocks which could possibly contribute light to this entity + int bMinX = MathHelper.floor_double(x1); + int bMinY = MathHelper.floor_double(y1); + int bMinZ = MathHelper.floor_double(z1); + int bMaxX = MathHelper.ceiling_double_int(x2); + int bMaxY = MathHelper.ceiling_double_int(y2); + int bMaxZ = MathHelper.ceiling_double_int(z2); + + // The maximum amount of light that could be contributed + double max = 0.0D; + + // The sampled light values contributed by all sources + double sl = 0; + double bl = 0; + + BlockPos.Mutable pos = new BlockPos.Mutable(); + + // Iterate over every block in the sampling volume + for (int bX = bMinX; bX < bMaxX; bX++) { + double ix1 = Math.max(bX, x1); + double ix2 = Math.min(bX + 1, x2); + + for (int bY = bMinY; bY < bMaxY; bY++) { + double iy1 = Math.max(bY, y1); + double iy2 = Math.min(bY + 1, y2); + + for (int bZ = bMinZ; bZ < bMaxZ; bZ++) { + pos.set(bX, bY, bZ); + + // Do not consider light-blocking volumes + if (entity.worldObj.getBlock(pos.x, pos.y, pos.z).isOpaqueCube() + && entity.worldObj.getBlockLightValue(pos.x, pos.y, pos.z) <= 0) { + continue; + } + + // Find the intersecting volume between the entity box and the block's bounding box + double iz1 = Math.max(bZ, z1); + double iz2 = Math.min(bZ + 1, z2); + + // The amount of light this block can contribute is the volume of the intersecting box + double weight = (ix2 - ix1) * (iy2 - iy1) * (iz2 - iz1); + + // Keep count of how much light could've been contributed + max += weight; + + // note: lighter.bridge$getSkyLight(entity, pos) and lighter.bridge$getBlockLight(entity, pos) + // were replaced, mixin+this method de-generified. as far as I can tell they only existed because + // Sodium had a weird setup just for paintings, don't think we need it. + + // Sum the light actually contributed by this volume + sl += weight * (entity.worldObj.getSkyBlockTypeBrightness(EnumSkyBlock.Sky, pos.x, pos.y, pos.z) / MAX_LIGHT_VAL); + + if (calcBlockLight) { + bl += weight * (entity.worldObj.getSkyBlockTypeBrightness(EnumSkyBlock.Block, pos.x, pos.y, pos.z) / MAX_LIGHT_VAL); + } else { + bl += weight; + } + } + } + } + + // The final light value is calculated from the percentage of light contributed out of the total maximum + int bli = MathHelper.floor_double((bl / max) * MAX_LIGHTMAP_COORD); + int sli = MathHelper.floor_double((sl / max) * MAX_LIGHTMAP_COORD); + + return ((sli & 0xFFFF) << 16) | (bli & 0xFFFF); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightMode.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightMode.java new file mode 100644 index 000000000..90133268c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightMode.java @@ -0,0 +1,6 @@ +package me.jellysquid.mods.sodium.client.model.light; + +public enum LightMode { + SMOOTH, + FLAT +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipeline.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipeline.java new file mode 100644 index 000000000..e81603d2c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipeline.java @@ -0,0 +1,22 @@ +package me.jellysquid.mods.sodium.client.model.light; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadView; +import net.minecraftforge.common.util.ForgeDirection; + +/** + * Light pipelines allow model quads for any location in the world to be lit using various backends, including fluids + * and block entities. + */ +public interface LightPipeline { + /** + * Calculates the light data for a given block model quad, storing the result in {@param out}. + * @param quad The block model quad + * @param pos The block position of the model this quad belongs to + * @param out The data arrays which will store the calculated light data results + * @param face The pre-computed facing vector of the quad + * @param shade True if the block is shaded by ambient occlusion + */ + void calculate(ModelQuadView quad, BlockPos pos, QuadLightData out, ForgeDirection cullFace, ForgeDirection face, boolean shade); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipelineProvider.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipelineProvider.java new file mode 100644 index 000000000..49fb74188 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/LightPipelineProvider.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.model.light; + +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import me.jellysquid.mods.sodium.client.model.light.flat.FlatLightPipeline; +import me.jellysquid.mods.sodium.client.model.light.smooth.SmoothLightPipeline; + +import java.util.EnumMap; + +public class LightPipelineProvider { + private final EnumMap lighters = new EnumMap<>(LightMode.class); + + public LightPipelineProvider(LightDataAccess cache) { + this.lighters.put(LightMode.SMOOTH, new SmoothLightPipeline(cache)); + this.lighters.put(LightMode.FLAT, new FlatLightPipeline(cache)); + } + + public LightPipeline getLighter(LightMode type) { + LightPipeline pipeline = this.lighters.get(type); + + if (pipeline == null) { + throw new NullPointerException("No lighter exists for mode: " + type.name()); + } + + return pipeline; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/ArrayLightDataCache.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/ArrayLightDataCache.java new file mode 100644 index 000000000..c7a445128 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/ArrayLightDataCache.java @@ -0,0 +1,56 @@ +package me.jellysquid.mods.sodium.client.model.light.cache; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import me.jellysquid.mods.sodium.client.world.WorldSlice; + +import java.util.Arrays; + +/** + * A light data cache which uses a flat-array to store the light data for the blocks in a given chunk and its direct + * neighbors. This is considerably faster than using a hash table to lookup values for a given block position and + * can be re-used by {@link WorldSlice} to avoid allocations. + */ +public class ArrayLightDataCache extends LightDataAccess { + private static final int NEIGHBOR_BLOCK_RADIUS = 2; + private static final int BLOCK_LENGTH = 16 + (NEIGHBOR_BLOCK_RADIUS * 2); + + private final long[] light; + + private int xOffset, yOffset, zOffset; + + public ArrayLightDataCache(WorldSlice world) { + this.world = world; + this.light = new long[BLOCK_LENGTH * BLOCK_LENGTH * BLOCK_LENGTH]; + } + + public void reset(ChunkSectionPos origin) { + this.xOffset = origin.getMinX() - NEIGHBOR_BLOCK_RADIUS; + this.yOffset = origin.getMinY() - NEIGHBOR_BLOCK_RADIUS; + this.zOffset = origin.getMinZ() - NEIGHBOR_BLOCK_RADIUS; + + Arrays.fill(this.light, 0L); + } + + private int index(int x, int y, int z) { + int x2 = x - this.xOffset; + int y2 = y - this.yOffset; + int z2 = z - this.zOffset; + + return (z2 * BLOCK_LENGTH * BLOCK_LENGTH) + (y2 * BLOCK_LENGTH) + x2; + } + + @Override + public long get(int x, int y, int z) { + int l = this.index(x, y, z); + + long word = this.light[l]; + + if (word != 0) { + return word; + } + + return this.light[l] = this.compute(x, y, z); + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/HashLightDataCache.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/HashLightDataCache.java new file mode 100644 index 000000000..275886426 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/cache/HashLightDataCache.java @@ -0,0 +1,38 @@ +package me.jellysquid.mods.sodium.client.model.light.cache; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import it.unimi.dsi.fastutil.longs.Long2LongLinkedOpenHashMap; +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import me.jellysquid.mods.sodium.client.world.WorldSlice; + + +/** + * A light data cache which uses a hash table to store previously accessed values. + */ +public class HashLightDataCache extends LightDataAccess { + private final Long2LongLinkedOpenHashMap map = new Long2LongLinkedOpenHashMap(1024, 0.50f); + + public HashLightDataCache(WorldSlice world) { + this.world = world; + } + + @Override + public long get(int x, int y, int z) { + long key = BlockPos.asLong(x, y, z); + long word = this.map.getAndMoveToFirst(key); + + if (word == 0) { + if (this.map.size() > 1024) { + this.map.removeLastLong(); + } + + this.map.put(key, word = this.compute(x, y, z)); + } + + return word; + } + + public void clearCache() { + this.map.clear(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/LightDataAccess.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/LightDataAccess.java new file mode 100644 index 000000000..246530460 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/LightDataAccess.java @@ -0,0 +1,124 @@ +package me.jellysquid.mods.sodium.client.model.light.data; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import net.minecraft.block.Block; +import net.minecraftforge.common.util.ForgeDirection; + +/** + * The light data cache is used to make accessing the light data and occlusion properties of blocks cheaper. The data + * for each block is stored as a long integer with packed fields in order to work around the lack of value types in Java. + * + * This code is not very pretty, but it does perform significantly faster than the vanilla implementation and has + * good cache locality. + * + * Each long integer contains the following fields: + * - OP: Block opacity test, true if opaque + * - FO: Full block opaque test, true if opaque + * - AO: Ambient occlusion, floating point value in the range of 0.0..1.0 encoded as an 12-bit unsigned integer + * - LM: Light map texture coordinates, two packed UV shorts in an integer + * + * You can use the various static pack/unpack methods to extract these values in a usable format. + */ +public abstract class LightDataAccess { + private final BlockPos.Mutable pos = new BlockPos.Mutable(); + protected WorldSlice world; + + public long get(int x, int y, int z, ForgeDirection d1, ForgeDirection d2) { + return this.get(x + d1.offsetX + d2.offsetX, y + d1.offsetY + d2.offsetY, z + d1.offsetZ + d2.offsetZ); + } + + public long get(int x, int y, int z, ForgeDirection dir) { + return this.get(x + dir.offsetX, y + dir.offsetY, z + dir.offsetZ); + } + + public long get(BlockPos pos, ForgeDirection dir) { + return this.get(pos.x, pos.y, pos.z, dir); + } + + public long get(BlockPos pos) { + return this.get(pos.x, pos.y, pos.getZ()); + } + + /** + * Returns the light data for the block at the given position. The property fields can then be accessed using + * the various unpack methods below. + */ + public abstract long get(int x, int y, int z); + + protected long compute(int x, int y, int z) { + final BlockPos pos = this.pos.set(x, y, z); + final WorldSlice world = this.world; + + final Block block = world.getBlock(x, y, z); + + final float ao; + final boolean em; + + if (block.getLightValue() == 0) { + ao = block.getAmbientOcclusionLightValue(); + em = false; //state.hasEmissiveLighting(world, pos); + } else { + ao = 1.0f; + em = true; + } + + // First is shouldBlockVision, but I can't find if any transparent objects set it + final boolean op = /*state.shouldBlockVision(world, pos) ||*/ block.getLightOpacity() == 0; + final boolean fo = block.isOpaqueCube(); + // Should be isFullCube, but this is probably close enough + final boolean fc = block.renderAsNormalBlock(); + + // OPTIMIZE: Do not calculate lightmap data if the block is full and opaque. + // FIX: Calculate lightmap data for light-emitting or emissive blocks, even though they are full and opaque. + final int lm = (fo && !em) ? 0 : block.getMixedBrightnessForBlock(world, x, y, z); + + return packAO(ao) | packLM(lm) | packOP(op) | packFO(fo) | packFC(fc) | (1L << 60); + } + + public static long packOP(boolean opaque) { + return (opaque ? 1L : 0L) << 56; + } + + public static boolean unpackOP(long word) { + return ((word >>> 56) & 0b1) != 0; + } + + public static long packFO(boolean opaque) { + return (opaque ? 1L : 0L) << 57; + } + + public static boolean unpackFO(long word) { + return ((word >>> 57) & 0b1) != 0; + } + + public static long packFC(boolean fullCube) { + return (fullCube ? 1L : 0L) << 58; + } + + public static boolean unpackFC(long word) { + return ((word >>> 58) & 0b1) != 0; + } + + public static long packLM(int lm) { + return (long) lm & 0xFFFFFFFFL; + } + + public static int unpackLM(long word) { + return (int) (word & 0xFFFFFFFFL); + } + + public static long packAO(float ao) { + int aoi = (int) (ao * 4096.0f); + return ((long) aoi & 0xFFFFL) << 32; + } + + public static float unpackAO(long word) { + int aoi = (int) (word >>> 32 & 0xFFFFL); + return aoi * (1.0f / 4096.0f); + } + + public WorldSlice getWorld() { + return this.world; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/QuadLightData.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/QuadLightData.java new file mode 100644 index 000000000..1342e1dcf --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/data/QuadLightData.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.client.model.light.data; + +/** + * Stores the computed light data for a block model quad. The vertex order of each array is defined as that of the + * quad's vertex order. + */ +public class QuadLightData { + /** + * The brightness of each vertex in the quad as normalized floats. + */ + public final float[] br = new float[4]; + + /** + * The lightmap texture coordinates for each vertex in the quad. + */ + public final int[] lm = new int[4]; +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java new file mode 100644 index 000000000..5d85b4f23 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java @@ -0,0 +1,70 @@ +package me.jellysquid.mods.sodium.client.model.light.flat; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import me.jellysquid.mods.sodium.client.model.light.LightPipeline; +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadView; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFlags; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.minecraftforge.common.util.ForgeDirection; + +import java.util.Arrays; + +/** + * A light pipeline which implements "classic-style" lighting through simply using the light value of the adjacent + * block to a face. + */ +public class FlatLightPipeline implements LightPipeline { + /** + * The cache which light data will be accessed from. + */ + private final LightDataAccess lightCache; + + public FlatLightPipeline(LightDataAccess lightCache) { + this.lightCache = lightCache; + } + + @Override + public void calculate(ModelQuadView quad, BlockPos pos, QuadLightData out, ForgeDirection cullFace, ForgeDirection face, boolean shade) { + int lightmap; + + // To match vanilla behavior, use the cull face if it exists/is available + if (cullFace != null) { + lightmap = getOffsetLightmap(pos, cullFace); + } else { + int flags = quad.getFlags(); + // If the face is aligned, use the light data above it + // To match vanilla behavior, also treat the face as aligned if it is parallel and the block state is a full cube + if ((flags & ModelQuadFlags.IS_ALIGNED) != 0 || ((flags & ModelQuadFlags.IS_PARALLEL) != 0 && LightDataAccess.unpackFC(this.lightCache.get(pos)))) { + lightmap = getOffsetLightmap(pos, face); + } else { + lightmap = LightDataAccess.unpackLM(this.lightCache.get(pos)); + } + } + + Arrays.fill(out.lm, lightmap); + final float brightness = (AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) + ? 1.0F : this.lightCache.getWorld().getBrightness(face, shade); + Arrays.fill(out.br, brightness); + } + + /** + * When vanilla computes an offset lightmap with flat lighting, it passes the original BlockState but the + * offset BlockPos to WorldRenderer#getLightmapCoordinates(BlockRenderView, BlockState, BlockPos) + * This does not make much sense but fixes certain issues, primarily dark quads on light-emitting blocks + * behind tinted glass. {@link LightDataAccess} cannot efficiently store lightmaps computed with + * inconsistent values so this method exists to mirror vanilla behavior as closely as possible. + */ + private int getOffsetLightmap(BlockPos pos, ForgeDirection face) { + int lightmap = LightDataAccess.unpackLM(this.lightCache.get(pos, face)); + // If the block light is not 15 (max)... + if ((lightmap & 0xF0) != 0xF0) { + int originLightmap = LightDataAccess.unpackLM(this.lightCache.get(pos)); + // ...take the maximum combined block light at the origin and offset positions + lightmap = (lightmap & ~0xFF) | Math.max(lightmap & 0xFF, originLightmap & 0xFF); + } + return lightmap; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoCompletionFlags.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoCompletionFlags.java new file mode 100644 index 000000000..4dd78bcc4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoCompletionFlags.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.client.model.light.smooth; + +/** + * Bit flags to indicate which light properties have been computed for a given face. + */ +class AoCompletionFlags { + /** + * The light data has been retrieved from the cache. + */ + public static final int HAS_LIGHT_DATA = 0b01; + + /** + * The light data has been unpacked into normalized floating point values. + */ + public static final int HAS_UNPACKED_LIGHT_DATA = 0b10; +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoFaceData.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoFaceData.java new file mode 100644 index 000000000..a6edf74e1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoFaceData.java @@ -0,0 +1,227 @@ +package me.jellysquid.mods.sodium.client.model.light.smooth; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import net.minecraftforge.common.util.ForgeDirection; + +import static me.jellysquid.mods.sodium.client.model.light.cache.ArrayLightDataCache.unpackAO; +import static me.jellysquid.mods.sodium.client.model.light.cache.ArrayLightDataCache.unpackFO; +import static me.jellysquid.mods.sodium.client.model.light.cache.ArrayLightDataCache.unpackLM; +import static me.jellysquid.mods.sodium.client.model.light.cache.ArrayLightDataCache.unpackOP; + +class AoFaceData { + public final int[] lm = new int[4]; + + public final float[] ao = new float[4]; + public final float[] bl = new float[4]; + public final float[] sl = new float[4]; + + private int flags; + + public void initLightData(LightDataAccess cache, BlockPos pos, ForgeDirection direction, boolean offset) { + final int x = pos.x; + final int y = pos.y; + final int z = pos.z; + + final int adjX; + final int adjY; + final int adjZ; + + if (offset) { + adjX = x + direction.offsetX; + adjY = y + direction.offsetY; + adjZ = z + direction.offsetZ; + } else { + adjX = x; + adjY = y; + adjZ = z; + } + + long adjWord = cache.get(adjX, adjY, adjZ); + + final int calm; + + // Use the origin block's light values if the adjacent one is opaque + if (offset && unpackFO(adjWord)) { + calm = unpackLM(cache.get(x, y, z)); + } else { + calm = unpackLM(adjWord); + } + + final float caao = unpackAO(adjWord); + + ForgeDirection[] faces = AoNeighborInfo.get(direction).faces; + + final long e0 = cache.get(adjX, adjY, adjZ, faces[0]); + final int e0lm = unpackLM(e0); + final float e0ao = unpackAO(e0); + final boolean e0op = unpackOP(e0); + + final long e1 = cache.get(adjX, adjY, adjZ, faces[1]); + final int e1lm = unpackLM(e1); + final float e1ao = unpackAO(e1); + final boolean e1op = unpackOP(e1); + + final long e2 = cache.get(adjX, adjY, adjZ, faces[2]); + final int e2lm = unpackLM(e2); + final float e2ao = unpackAO(e2); + final boolean e2op = unpackOP(e2); + + final long e3 = cache.get(adjX, adjY, adjZ, faces[3]); + final int e3lm = unpackLM(e3); + final float e3ao = unpackAO(e3); + final boolean e3op = unpackOP(e3); + + // If neither edge of a corner is occluded, then use the light + final int c0lm; + final float c0ao; + + if (!e2op && !e0op) { + c0lm = e0lm; + c0ao = e0ao; + } else { + long d0 = cache.get(adjX, adjY, adjZ, faces[0], faces[2]); + c0lm = unpackLM(d0); + c0ao = unpackAO(d0); + } + + final int c1lm; + final float c1ao; + + if (!e3op && !e0op) { + c1lm = e0lm; + c1ao = e0ao; + } else { + long d1 = cache.get(adjX, adjY, adjZ, faces[0], faces[3]); + c1lm = unpackLM(d1); + c1ao = unpackAO(d1); + } + + final int c2lm; + final float c2ao; + + if (!e2op && !e1op) { + // FIX: Use e1 instead of c0 to fix lighting errors in some directions + c2lm = e1lm; + c2ao = e1ao; + } else { + long d2 = cache.get(adjX, adjY, adjZ, faces[1], faces[2]); + c2lm = unpackLM(d2); + c2ao = unpackAO(d2); + } + + final int c3lm; + final float c3ao; + + if (!e3op && !e1op) { + // FIX: Use e1 instead of c0 to fix lighting errors in some directions + c3lm = e1lm; + c3ao = e1ao; + } else { + long d3 = cache.get(adjX, adjY, adjZ, faces[1], faces[3]); + c3lm = unpackLM(d3); + c3ao = unpackAO(d3); + } + + float[] ao = this.ao; + ao[0] = (e3ao + e0ao + c1ao + caao) * 0.25f; + ao[1] = (e2ao + e0ao + c0ao + caao) * 0.25f; + ao[2] = (e2ao + e1ao + c2ao + caao) * 0.25f; + ao[3] = (e3ao + e1ao + c3ao + caao) * 0.25f; + + int[] cb = this.lm; + cb[0] = calculateCornerBrightness(e3lm, e0lm, c1lm, calm); + cb[1] = calculateCornerBrightness(e2lm, e0lm, c0lm, calm); + cb[2] = calculateCornerBrightness(e2lm, e1lm, c2lm, calm); + cb[3] = calculateCornerBrightness(e3lm, e1lm, c3lm, calm); + + this.flags |= AoCompletionFlags.HAS_LIGHT_DATA; + } + + public void unpackLightData() { + int[] lm = this.lm; + + float[] bl = this.bl; + float[] sl = this.sl; + + bl[0] = unpackBlockLight(lm[0]); + bl[1] = unpackBlockLight(lm[1]); + bl[2] = unpackBlockLight(lm[2]); + bl[3] = unpackBlockLight(lm[3]); + + sl[0] = unpackSkyLight(lm[0]); + sl[1] = unpackSkyLight(lm[1]); + sl[2] = unpackSkyLight(lm[2]); + sl[3] = unpackSkyLight(lm[3]); + + this.flags |= AoCompletionFlags.HAS_UNPACKED_LIGHT_DATA; + } + + public float getBlendedSkyLight(float[] w) { + return weightedSum(this.sl, w); + } + + public float getBlendedBlockLight(float[] w) { + return weightedSum(this.bl, w); + } + + public float getBlendedShade(float[] w) { + return weightedSum(this.ao, w); + } + + private static float weightedSum(float[] v, float[] w) { + float t0 = v[0] * w[0]; + float t1 = v[1] * w[1]; + float t2 = v[2] * w[2]; + float t3 = v[3] * w[3]; + + return t0 + t1 + t2 + t3; + } + + private static float unpackSkyLight(int i) { + return (i >> 16) & 0xFF; + } + + private static float unpackBlockLight(int i) { + return i & 0xFF; + } + + private static int calculateCornerBrightness(int a, int b, int c, int d) { + // FIX: Normalize corner vectors correctly to the minimum non-zero value between each one to prevent + // strange issues + if ((a == 0) || (b == 0) || (c == 0) || (d == 0)) { + // Find the minimum value between all corners + final int min = minNonZero(minNonZero(a, b), minNonZero(c, d)); + + // Normalize the corner values + a = Math.max(a, min); + b = Math.max(b, min); + c = Math.max(c, min); + d = Math.max(d, min); + } + + return ((a + b + c + d) >> 2) & 0xFF00FF; + } + + private static int minNonZero(int a, int b) { + if (a == 0) { + return b; + } else if (b == 0) { + return a; + } + + return Math.min(a, b); + } + + public boolean hasLightData() { + return (this.flags & AoCompletionFlags.HAS_LIGHT_DATA) != 0; + } + + public boolean hasUnpackedLightData() { + return (this.flags & AoCompletionFlags.HAS_UNPACKED_LIGHT_DATA) != 0; + } + + public void reset() { + this.flags = 0; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoNeighborInfo.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoNeighborInfo.java new file mode 100644 index 000000000..f9ce5def4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/AoNeighborInfo.java @@ -0,0 +1,249 @@ +package me.jellysquid.mods.sodium.client.model.light.smooth; + +import net.minecraftforge.common.util.ForgeDirection; + +/** + * The neighbor information for each face of a block, used when performing smooth lighting in order to calculate + * the occlusion of each corner. + */ +enum AoNeighborInfo { + DOWN(new ForgeDirection[] { ForgeDirection.WEST, ForgeDirection.EAST, ForgeDirection.NORTH, ForgeDirection.SOUTH }, 0.5F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = z; + final float v = 1.0f - x; + + out[0] = v * u; + out[1] = v * (1.0f - u); + out[2] = (1.0f - v) * (1.0f - u); + out[3] = (1.0f - v) * u; + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[0] = lm0[0]; + lm1[1] = lm0[1]; + lm1[2] = lm0[2]; + lm1[3] = lm0[3]; + + ao1[0] = ao0[0]; + ao1[1] = ao0[1]; + ao1[2] = ao0[2]; + ao1[3] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return y; + } + }, + UP(new ForgeDirection[] { ForgeDirection.EAST, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.SOUTH }, 1.0F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = z; + final float v = x; + + out[0] = v * u; + out[1] = v * (1.0f - u); + out[2] = (1.0f - v) * (1.0f - u); + out[3] = (1.0f - v) * u; + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[2] = lm0[0]; + lm1[3] = lm0[1]; + lm1[0] = lm0[2]; + lm1[1] = lm0[3]; + + ao1[2] = ao0[0]; + ao1[3] = ao0[1]; + ao1[0] = ao0[2]; + ao1[1] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return 1.0f - y; + } + }, + NORTH(new ForgeDirection[] { ForgeDirection.UP, ForgeDirection.DOWN, ForgeDirection.EAST, ForgeDirection.WEST }, 0.8F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = 1.0f - x; + final float v = y; + + out[0] = v * u; + out[1] = v * (1.0f - u); + out[2] = (1.0f - v) * (1.0f - u); + out[3] = (1.0f - v) * u; + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[3] = lm0[0]; + lm1[0] = lm0[1]; + lm1[1] = lm0[2]; + lm1[2] = lm0[3]; + + ao1[3] = ao0[0]; + ao1[0] = ao0[1]; + ao1[1] = ao0[2]; + ao1[2] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return z; + } + }, + SOUTH(new ForgeDirection[] { ForgeDirection.WEST, ForgeDirection.EAST, ForgeDirection.DOWN, ForgeDirection.UP }, 0.8F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = y; + final float v = 1.0f - x; + + out[0] = u * v; + out[1] = (1.0f - u) * v; + out[2] = (1.0f - u) * (1.0f - v); + out[3] = u * (1.0f - v); + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[0] = lm0[0]; + lm1[1] = lm0[1]; + lm1[2] = lm0[2]; + lm1[3] = lm0[3]; + + ao1[0] = ao0[0]; + ao1[1] = ao0[1]; + ao1[2] = ao0[2]; + ao1[3] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return 1.0f - z; + } + }, + WEST(new ForgeDirection[] { ForgeDirection.UP, ForgeDirection.DOWN, ForgeDirection.NORTH, ForgeDirection.SOUTH }, 0.6F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = z; + final float v = y; + + out[0] = v * u; + out[1] = v * (1.0f - u); + out[2] = (1.0f - v) * (1.0f - u); + out[3] = (1.0f - v) * u; + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[3] = lm0[0]; + lm1[0] = lm0[1]; + lm1[1] = lm0[2]; + lm1[2] = lm0[3]; + + ao1[3] = ao0[0]; + ao1[0] = ao0[1]; + ao1[1] = ao0[2]; + ao1[2] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return x; + } + }, + EAST(new ForgeDirection[] { ForgeDirection.DOWN, ForgeDirection.UP, ForgeDirection.NORTH, ForgeDirection.SOUTH }, 0.6F) { + @Override + public void calculateCornerWeights(float x, float y, float z, float[] out) { + final float u = z; + final float v = 1.0f - y; + + out[0] = v * u; + out[1] = v * (1.0f - u); + out[2] = (1.0f - v) * (1.0f - u); + out[3] = (1.0f - v) * u; + } + + @Override + public void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1) { + lm1[1] = lm0[0]; + lm1[2] = lm0[1]; + lm1[3] = lm0[2]; + lm1[0] = lm0[3]; + + ao1[1] = ao0[0]; + ao1[2] = ao0[1]; + ao1[3] = ao0[2]; + ao1[0] = ao0[3]; + } + + @Override + public float getDepth(float x, float y, float z) { + return 1.0f - x; + } + }; + + /** + * The direction of each corner block from this face, which can be retrieved by offsetting the position of the origin + * block by the direction vector. + */ + public final ForgeDirection[] faces; + + /** + * The constant brightness modifier for this face. This data exists to emulate the results of the OpenGL lighting + * model which gives a faux directional light appearance to blocks in the game. Not currently used. + */ + public final float strength; + + AoNeighborInfo(ForgeDirection[] directions, float strength) { + this.faces = directions; + this.strength = strength; + } + + /** + * Calculates how much each corner contributes to the final "darkening" of the vertex at the specified position. The + * weight is a function of the distance from the vertex's position to the corner block's position. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param out The weight values for each corner + */ + public abstract void calculateCornerWeights(float x, float y, float z, float[] out); + + /** + * Maps the light map and occlusion value arrays {@param lm0} and {@param ao0} from {@link AoFaceData} to the + * correct corners for this facing. + * + * @param lm0 The input light map texture coordinates array + * @param ao0 The input ambient occlusion color array + * @param lm1 The re-orientated output light map texture coordinates array + * @param ao1 The re-orientated output ambient occlusion color array + */ + public abstract void mapCorners(int[] lm0, float[] ao0, int[] lm1, float[] ao1); + + /** + * Calculates the depth (or inset) of the vertex into this facing of the block. Used to determine + * how much shadow is contributed by the direct neighbors of a block. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @return The depth of the vertex into this face + */ + public abstract float getDepth(float x, float y, float z); + + private static final AoNeighborInfo[] VALUES = AoNeighborInfo.values(); + + /** + * @return Returns the {@link AoNeighborInfo} which corresponds with the specified direction + */ + public static AoNeighborInfo get(ForgeDirection direction) { + return VALUES[direction.ordinal()]; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java new file mode 100644 index 000000000..dbc6ef5c4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java @@ -0,0 +1,278 @@ +package me.jellysquid.mods.sodium.client.model.light.smooth; + +import com.google.common.math.DoubleMath; +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import me.jellysquid.mods.sodium.client.model.light.LightPipeline; +import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; +import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadView; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFlags; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.minecraftforge.common.util.ForgeDirection; + + +/** + * A light pipeline which produces smooth interpolated lighting and ambient occlusion for model quads. This + * implementation makes a number of improvements over vanilla's own "smooth lighting" option. In no particular order: + * + * - Ambient occlusion of block slopes underwater no longer produces broken results (fixes MC-149211) + * - Smooth lighting now works when underwater (fixes MC-68129) + * - Corner blocks are now selected from the correct set of neighbors above block faces (fixes MC-148689 and MC-12558) + * - Shading issues caused by anisotropy are fixed by re-orientating quads to a consistent ordering (fixes MC-136302) + * - Inset block faces are correctly shaded by their neighbors, fixing a number of problems with non-full blocks such as + * grass paths (fixes MC-11783 and MC-108621) + * - Synchronization issues between the main render thread's light engine and chunk build worker threads are corrected + * by copying light data alongside block states, fixing a number of inconsistencies in baked chunks (no open issue) + * + * This implementation also includes a significant number of optimizations: + * + * - Computed light data for a given block face is cached and re-used again when multiple quads exist for a given + * facing, making complex block models less expensive to render + * - The light data cache encodes as much information as possible into integer words to improve cache locality and + * to eliminate the multiple array lookups that would otherwise be needed, significantly speeding up this section + * - Block faces aligned to the block grid use a fast-path for mapping corner light values to vertices without expensive + * interpolation or blending, speeding up most block renders + * - Some critical code paths have been re-written to hit the JVM's happy path, allowing it to perform auto-vectorization + * of the blend functions + * - Information about a given model quad is cached to enable the light pipeline to make certain assumptions and skip + * unnecessary computation + */ +public class SmoothLightPipeline implements LightPipeline { + public static final double TOLERANCE = 1.0E-5F; + /** + * The cache which light data will be accessed from. + */ + private final LightDataAccess lightCache; + + /** + * The cached face data for each side of a block, both inset and outset. + */ + private final AoFaceData[] cachedFaceData = new AoFaceData[6 * 2]; + + /** + * The position at which the cached face data was taken at. + */ + private long cachedPos = Long.MIN_VALUE; + + /** + * A temporary array for storing the intermediary results of weight data for non-aligned face blending. + */ + private final float[] weights = new float[4]; + + public SmoothLightPipeline(LightDataAccess cache) { + this.lightCache = cache; + + for (int i = 0; i < this.cachedFaceData.length; i++) { + this.cachedFaceData[i] = new AoFaceData(); + } + } + + @Override + public void calculate(ModelQuadView quad, BlockPos pos, QuadLightData out, ForgeDirection cullFace, ForgeDirection face, boolean shade) { + this.updateCachedData(pos.asLong()); + + int flags = quad.getFlags(); + + final AoNeighborInfo neighborInfo = AoNeighborInfo.get(face); + + // If the model quad is aligned to the block's face and covers it entirely, we can take a fast path and directly + // map the corner values onto this quad's vertices. This covers most situations during rendering and provides + // a modest speed-up. + // To match vanilla behavior, also treat the face as aligned if it is parallel and the block state is a full cube + if ((flags & ModelQuadFlags.IS_ALIGNED) != 0 || ((flags & ModelQuadFlags.IS_PARALLEL) != 0 && LightDataAccess.unpackFC(this.lightCache.get(pos)))) { + if ((flags & ModelQuadFlags.IS_PARTIAL) == 0) { + this.applyAlignedFullFace(neighborInfo, pos, face, out); + } else { + this.applyAlignedPartialFace(neighborInfo, quad, pos, face, out); + } + } else if ((flags & ModelQuadFlags.IS_PARALLEL) != 0) { + this.applyParallelFace(neighborInfo, quad, pos, face, out); + } else { + this.applyNonParallelFace(neighborInfo, quad, pos, face, out); + } + + this.applySidedBrightness(out, face, shade); + } + + /** + * Quickly calculates the light data for a full grid-aligned quad. This represents the most common case (outward + * facing quads on a full-block model) and avoids interpolation between neighbors as each corner will only ever + * have two contributing sides. + * Flags: IS_ALIGNED, !IS_PARTIAL + */ + private void applyAlignedFullFace(AoNeighborInfo neighborInfo, BlockPos pos, ForgeDirection dir, QuadLightData out) { + AoFaceData faceData = this.getCachedFaceData(pos, dir, true); + neighborInfo.mapCorners(faceData.lm, faceData.ao, out.lm, out.br); + } + + /** + * Calculates the light data for a grid-aligned quad that does not cover the entire block volume's face. + * Flags: IS_ALIGNED, IS_PARTIAL + */ + private void applyAlignedPartialFace(AoNeighborInfo neighborInfo, ModelQuadView quad, BlockPos pos, ForgeDirection dir, QuadLightData out) { + for (int i = 0; i < 4; i++) { + // Clamp the vertex positions to the block's boundaries to prevent weird errors in lighting + float cx = clamp(quad.getX(i)); + float cy = clamp(quad.getY(i)); + float cz = clamp(quad.getZ(i)); + + float[] weights = this.weights; + neighborInfo.calculateCornerWeights(cx, cy, cz, weights); + this.applyAlignedPartialFaceVertex(pos, dir, weights, i, out, true); + } + } + + /** + * This method is the same as {@link #applyNonParallelFace(AoNeighborInfo, ModelQuadView, BlockPos, ForgeDirection, + * QuadLightData)} but with the check for a depth of approximately 0 removed. If the quad is parallel but not + * aligned, all of its vertices will have the same depth and this depth must be approximately greater than 0, + * meaning the check for 0 will always return false. + * Flags: !IS_ALIGNED, IS_PARALLEL + */ + private void applyParallelFace(AoNeighborInfo neighborInfo, ModelQuadView quad, BlockPos pos, ForgeDirection dir, QuadLightData out) { + for (int i = 0; i < 4; i++) { + // Clamp the vertex positions to the block's boundaries to prevent weird errors in lighting + float cx = clamp(quad.getX(i)); + float cy = clamp(quad.getY(i)); + float cz = clamp(quad.getZ(i)); + + float[] weights = this.weights; + neighborInfo.calculateCornerWeights(cx, cy, cz, weights); + + float depth = neighborInfo.getDepth(cx, cy, cz); + + // If the quad is approximately grid-aligned (not inset) to the other side of the block, avoid unnecessary + // computation by treating it is as aligned + if (DoubleMath.fuzzyEquals(depth, 1.0F, TOLERANCE)) { + this.applyAlignedPartialFaceVertex(pos, dir, weights, i, out, false); + } else { + // Blend the occlusion factor between the blocks directly beside this face and the blocks above it + // based on how inset the face is. This fixes a few issues with blocks such as farmland and paths. + this.applyInsetPartialFaceVertex(pos, dir, depth, 1.0f - depth, weights, i, out); + } + } + } + + /** + * Flags: !IS_ALIGNED, !IS_PARALLEL + */ + private void applyNonParallelFace(AoNeighborInfo neighborInfo, ModelQuadView quad, BlockPos pos, ForgeDirection dir, QuadLightData out) { + for (int i = 0; i < 4; i++) { + // Clamp the vertex positions to the block's boundaries to prevent weird errors in lighting + float cx = clamp(quad.getX(i)); + float cy = clamp(quad.getY(i)); + float cz = clamp(quad.getZ(i)); + + float[] weights = this.weights; + neighborInfo.calculateCornerWeights(cx, cy, cz, weights); + + float depth = neighborInfo.getDepth(cx, cy, cz); + + // If the quad is approximately grid-aligned (not inset), avoid unnecessary computation by treating it is as aligned + if (DoubleMath.fuzzyEquals(depth, 0.0F, TOLERANCE)) { + this.applyAlignedPartialFaceVertex(pos, dir, weights, i, out, true); + } else if (DoubleMath.fuzzyEquals(depth, 1.0F, TOLERANCE)) { + this.applyAlignedPartialFaceVertex(pos, dir, weights, i, out, false); + } else { + // Blend the occlusion factor between the blocks directly beside this face and the blocks above it + // based on how inset the face is. This fixes a few issues with blocks such as farmland and paths. + this.applyInsetPartialFaceVertex(pos, dir, depth, 1.0f - depth, weights, i, out); + } + } + } + + private void applyAlignedPartialFaceVertex(BlockPos pos, ForgeDirection dir, float[] w, int i, QuadLightData out, boolean offset) { + AoFaceData faceData = this.getCachedFaceData(pos, dir, offset); + + if (!faceData.hasUnpackedLightData()) { + faceData.unpackLightData(); + } + + float sl = faceData.getBlendedSkyLight(w); + float bl = faceData.getBlendedBlockLight(w); + float ao = faceData.getBlendedShade(w); + + out.br[i] = ao; + out.lm[i] = getLightMapCoord(sl, bl); + } + + private void applyInsetPartialFaceVertex(BlockPos pos, ForgeDirection dir, float n1d, float n2d, float[] w, int i, QuadLightData out) { + AoFaceData n1 = this.getCachedFaceData(pos, dir, false); + + if (!n1.hasUnpackedLightData()) { + n1.unpackLightData(); + } + + AoFaceData n2 = this.getCachedFaceData(pos, dir, true); + + if (!n2.hasUnpackedLightData()) { + n2.unpackLightData(); + } + + // Blend between the direct neighbors and above based on the passed weights + float ao = (n1.getBlendedShade(w) * n1d) + (n2.getBlendedShade(w) * n2d); + float sl = (n1.getBlendedSkyLight(w) * n1d) + (n2.getBlendedSkyLight(w) * n2d); + float bl = (n1.getBlendedBlockLight(w) * n1d) + (n2.getBlendedBlockLight(w) * n2d); + + out.br[i] = ao; + out.lm[i] = getLightMapCoord(sl, bl); + } + + private void applySidedBrightness(QuadLightData out, ForgeDirection face, boolean shade) { + if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) { + return; + } + + float brightness = this.lightCache.getWorld().getBrightness(face, shade); + float[] br = out.br; + + for (int i = 0; i < br.length; i++) { + br[i] *= brightness; + } + } + + /** + * Returns the cached data for a given facing or calculates it if it hasn't been cached. + */ + private AoFaceData getCachedFaceData(BlockPos pos, ForgeDirection face, boolean offset) { + AoFaceData data = this.cachedFaceData[offset ? face.ordinal() : face.ordinal() + 6]; + + if (!data.hasLightData()) { + data.initLightData(this.lightCache, pos, face, offset); + } + + return data; + } + + private void updateCachedData(long key) { + if (this.cachedPos != key) { + for (AoFaceData data : this.cachedFaceData) { + data.reset(); + } + + this.cachedPos = key; + } + } + + /** + * Clamps the given float to the range [0.0, 1.0]. + */ + private static float clamp(float v) { + if (v < 0.0f) { + return 0.0f; + } else if (v > 1.0f) { + return 1.0f; + } + + return v; + } + + /** + * Returns a texture coordinate on the light map texture for the given block and sky light values. + */ + private static int getLightMapCoord(float sl, float bl) { + return (((int) sl & 0xFF) << 16) | ((int) bl & 0xFF); + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuad.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuad.java new file mode 100644 index 000000000..16cd79869 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuad.java @@ -0,0 +1,139 @@ +package me.jellysquid.mods.sodium.client.model.quad; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.COLOR_INDEX; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.LIGHT_INDEX; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.NORMAL_INDEX; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.POSITION_INDEX; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.TEXTURE_INDEX; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.VERTEX_SIZE; +import static me.jellysquid.mods.sodium.client.util.ModelQuadUtil.vertexOffset; + +/** + * A simple implementation of the {@link ModelQuadViewMutable} interface which can provide an on-heap scratch area + * for storing quad vertex data. + */ +public class ModelQuad implements ModelQuadViewMutable { + private final int[] data = new int[VERTEX_SIZE * 4]; + private int flags; + + private TextureAtlasSprite sprite; + private int colorIdx; + + @Override + public void setX(int idx, float x) { + this.data[vertexOffset(idx) + POSITION_INDEX] = Float.floatToRawIntBits(x); + } + + @Override + public void setY(int idx, float y) { + this.data[vertexOffset(idx) + POSITION_INDEX + 1] = Float.floatToRawIntBits(y); + } + + @Override + public void setZ(int idx, float z) { + this.data[vertexOffset(idx) + POSITION_INDEX + 2] = Float.floatToRawIntBits(z); + } + + @Override + public void setColor(int idx, int color) { + this.data[vertexOffset(idx) + COLOR_INDEX] = color; + } + + @Override + public void setTexU(int idx, float u) { + this.data[vertexOffset(idx) + TEXTURE_INDEX] = Float.floatToRawIntBits(u); + } + + @Override + public void setTexV(int idx, float v) { + this.data[vertexOffset(idx) + TEXTURE_INDEX + 1] = Float.floatToRawIntBits(v); + } + + @Override + public void setLight(int idx, int light) { + this.data[vertexOffset(idx) + LIGHT_INDEX] = light; + } + + @Override + public void setNormal(int idx, int norm) { + this.data[vertexOffset(idx) + NORMAL_INDEX] = norm; + } + + @Override + public void setFlags(int flags) { + this.flags = flags; + } + + @Override + public void setSprite(TextureAtlasSprite sprite) { + this.sprite = sprite; + } + + @Override + public void setColorIndex(int index) { + this.colorIdx = index; + } + + @Override + public int getLight(int idx) { + return this.data[vertexOffset(idx) + LIGHT_INDEX]; + } + + @Override + public int getNormal(int idx) { + return this.data[vertexOffset(idx) + NORMAL_INDEX]; + } + + @Override + public int getColorIndex() { + return this.colorIdx; + } + + @Override + public float getX(int idx) { + return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX]); + } + + @Override + public float getY(int idx) { + return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 1]); + } + + @Override + public float getZ(int idx) { + return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 2]); + } + + @Override + public int getColor(int idx) { + if(vertexOffset(idx) + COLOR_INDEX < data.length) { + return this.data[vertexOffset(idx) + COLOR_INDEX]; + } + else { + return data.length; + } + } + + @Override + public float getTexU(int idx) { + return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX]); + } + + @Override + public float getTexV(int idx) { + return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX + 1]); + } + + @Override + public int getFlags() { + return this.flags; + } + + @Override + public TextureAtlasSprite rubidium$getSprite() { + return this.sprite; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadView.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadView.java new file mode 100644 index 000000000..684339dd1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadView.java @@ -0,0 +1,85 @@ +package me.jellysquid.mods.sodium.client.model.quad; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFlags; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +import java.nio.ByteBuffer; + +/** + * Provides a read-only view of a model quad. For mutable access to a model quad, see {@link ModelQuadViewMutable}. + */ +public interface ModelQuadView { + /** + * @return The x-position of the vertex at index {@param idx} + */ + float getX(int idx); + + /** + * @return The y-position of the vertex at index {@param idx} + */ + float getY(int idx); + + /** + * @return The z-position of the vertex at index {@param idx} + */ + float getZ(int idx); + + /** + * @return The integer-encoded color of the vertex at index {@param idx} + */ + int getColor(int idx); + + /** + * @return The texture x-coordinate for the vertex at index {@param idx} + */ + float getTexU(int idx); + + /** + * @return The texture y-coordinate for the vertex at index {@param idx} + */ + float getTexV(int idx); + + /** + * @return The integer bit flags containing the {@link ModelQuadFlags} for this quad + */ + int getFlags(); + + /** + * @return The lightmap texture coordinates for the vertex at index {@param idx} + */ + int getLight(int idx); + + /** + * @return The integer-encoded normal vector for the vertex at index {@param idx} + */ + int getNormal(int idx); + + /** + * @return The color index of this quad. + */ + int getColorIndex(); + + /** + * Copies this quad's data into the specified buffer starting at the given position. + * @param buf The buffer to write this quad's data to + * @param position The starting byte index to write to + */ + default void copyInto(ByteBuffer buf, int position) { + for (int i = 0; i < 4; i++) { + buf.putFloat(position, this.getX(i)); + buf.putFloat(position + 4, this.getY(i)); + buf.putFloat(position + 8, this.getZ(i)); + buf.putInt(position + 12, this.getColor(i)); + buf.putFloat(position + 16, this.getTexU(i)); + buf.putFloat(position + 20, this.getTexV(i)); + buf.putInt(position + 24, this.getLight(i)); + + position += 28; + } + } + + /** + * @return The sprite texture used by this quad, or null if none is attached + */ + TextureAtlasSprite rubidium$getSprite(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadViewMutable.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadViewMutable.java new file mode 100644 index 000000000..42e2db755 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/ModelQuadViewMutable.java @@ -0,0 +1,64 @@ +package me.jellysquid.mods.sodium.client.model.quad; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFlags; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +/** + * Provides a mutable view to a model quad. + */ +public interface ModelQuadViewMutable extends ModelQuadView { + /** + * Sets the x-position of the vertex at index {@param idx} to the value {@param x} + */ + void setX(int idx, float x); + + /** + * Sets the y-position of the vertex at index {@param idx} to the value {@param y} + */ + void setY(int idx, float y); + + /** + * Sets the z-position of the vertex at index {@param idx} to the value {@param z} + */ + void setZ(int idx, float z); + + /** + * Sets the integer-encoded color of the vertex at index {@param idx} to the value {@param color} + */ + void setColor(int idx, int color); + + /** + * Sets the texture x-coordinate of the vertex at index {@param idx} to the value {@param u} + */ + void setTexU(int idx, float u); + + /** + * Sets the texture y-coordinate of the vertex at index {@param idx} to the value {@param v} + */ + void setTexV(int idx, float v); + + /** + * Sets the light map texture coordinate of the vertex at index {@param idx} to the value {@param light} + */ + void setLight(int idx, int light); + + /** + * Sets the integer-encoded normal vector of the vertex at index {@param idx} to the value {@param light} + */ + void setNormal(int idx, int norm); + + /** + * Sets the bit-flag field which contains the {@link ModelQuadFlags} for this quad + */ + void setFlags(int flags); + + /** + * Sets the sprite used by this quad + */ + void setSprite(TextureAtlasSprite sprite); + + /** + * Sets the color index used by this quad + */ + void setColorIndex(int index); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFacing.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFacing.java new file mode 100644 index 000000000..c8f740508 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFacing.java @@ -0,0 +1,78 @@ +package me.jellysquid.mods.sodium.client.model.quad.properties; + +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3f; + +public enum ModelQuadFacing { + UP, + DOWN, + EAST, + WEST, + SOUTH, + NORTH, + UNASSIGNED; + + public static final ModelQuadFacing[] VALUES = ModelQuadFacing.values(); + public static final int COUNT = VALUES.length; + + public static ModelQuadFacing fromDirection(ForgeDirection dir) { + return switch (dir) { + case DOWN -> DOWN; + case UP -> UP; + case NORTH -> NORTH; + case SOUTH -> SOUTH; + case WEST -> WEST; + case EAST -> EAST; + default -> UNASSIGNED; + }; + } + + public static ForgeDirection toDirection(ModelQuadFacing dir) { + return switch (dir) { + case DOWN -> ForgeDirection.DOWN; + case UP -> ForgeDirection.UP; + case NORTH -> ForgeDirection.NORTH; + case SOUTH -> ForgeDirection.SOUTH; + case WEST -> ForgeDirection.WEST; + case EAST -> ForgeDirection.EAST; + default -> ForgeDirection.UNKNOWN; + }; + } + + public static ModelQuadFacing fromVector(Vector3f normal) { + if(normal.x == 0f) { + if(normal.y == 0f) { + if(normal.z > 0) { + return SOUTH; + } else if(normal.z < 0) { + return NORTH; + } + } else if(normal.z == 0f) { + if(normal.y > 0) { + return UP; + } else if(normal.y < 0) { + return DOWN; + } + } + } else if(normal.y == 0f && (normal.z == 0f)) { + if(normal.x > 0) { + return EAST; + } else if(normal.x < 0) { + return WEST; + } + } + return UNASSIGNED; + } + + public ModelQuadFacing getOpposite() { + return switch (this) { + case UP -> DOWN; + case DOWN -> UP; + case EAST -> WEST; + case WEST -> EAST; + case SOUTH -> NORTH; + case NORTH -> SOUTH; + default -> UNASSIGNED; + }; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFlags.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFlags.java new file mode 100644 index 000000000..6fd3dbdb7 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadFlags.java @@ -0,0 +1,100 @@ +package me.jellysquid.mods.sodium.client.model.quad.properties; + +import com.gtnewhorizons.angelica.compat.mojang.Axis; +import com.gtnewhorizons.angelica.compat.nd.Quad; +import net.minecraftforge.common.util.ForgeDirection; + +public class ModelQuadFlags { + /** + * Indicates that the quad does not fully cover the given face for the model. + */ + public static final int IS_PARTIAL = 0b001; + + /** + * Indicates that the quad is parallel to its light face. + */ + public static final int IS_PARALLEL = 0b010; + + /** + * Indicates that the quad is aligned to the block grid. + * This flag is only set if {@link #IS_PARALLEL} is set. + */ + public static final int IS_ALIGNED = 0b100; + + /** + * @return True if the bit-flag of {@link ModelQuadFlags} contains the given flag + */ + public static boolean contains(int flags, int mask) { + return (flags & mask) != 0; + } + + /** + * Calculates the properties of the given quad. This data is used later by the light pipeline in order to make + * certain optimizations. + */ + public static int getQuadFlags(Quad quad) { + final ForgeDirection face = quad.getFace(); + final Axis axis = Axis.fromDirection(quad.normal); + + float minX = 32.0F; + float minY = 32.0F; + float minZ = 32.0F; + + float maxX = -32.0F; + float maxY = -32.0F; + float maxZ = -32.0F; + + for (int i = 0; i < 4; ++i) { + float x = quad.getX(i); + float y = quad.getY(i); + float z = quad.getZ(i); + + minX = Math.min(minX, x); + minY = Math.min(minY, y); + minZ = Math.min(minZ, z); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + maxZ = Math.max(maxZ, z); + } + + boolean partial = switch (axis) { + case X -> minY >= 0.0001f || minZ >= 0.0001f || maxY <= 0.9999F || maxZ <= 0.9999F; + case Y -> minX >= 0.0001f || minZ >= 0.0001f || maxX <= 0.9999F || maxZ <= 0.9999F; + case Z -> minX >= 0.0001f || minY >= 0.0001f || maxX <= 0.9999F || maxY <= 0.9999F; + }; + + boolean parallel = switch (axis) { + case X -> minX == maxX; + case Y -> minY == maxY; + case Z -> minZ == maxZ; + }; + + boolean flag = switch (face) { + case DOWN -> minY < 0.0001f; + case UP -> maxY > 0.9999F; + case NORTH -> minZ < 0.0001f; + case SOUTH -> maxZ > 0.9999F; + case WEST -> minX < 0.0001f; + case EAST -> maxX > 0.9999F; + default -> false; + }; + + boolean aligned = parallel && flag; + + int flags = 0; + + if (partial) { + flags |= IS_PARTIAL; + } + + if (parallel) { + flags |= IS_PARALLEL; + } + + if (aligned) { + flags |= IS_ALIGNED; + } + + return flags; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadOrientation.java b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadOrientation.java new file mode 100644 index 000000000..662ce4aaf --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/quad/properties/ModelQuadOrientation.java @@ -0,0 +1,35 @@ +package me.jellysquid.mods.sodium.client.model.quad.properties; + +/** + * Defines the orientation of vertices in a model quad. This information be used to re-orient the quad's vertices to a + * consistent order, eliminating a number of shading issues caused by anisotropy problems. + */ +public enum ModelQuadOrientation { + NORMAL(new int[] { 0, 1, 2, 3 }), + FLIP(new int[] { 1, 2, 3, 0 }); + + private final int[] indices; + + ModelQuadOrientation(int[] indices) { + this.indices = indices; + } + + /** + * @return The re-oriented index of the vertex {@param idx} + */ + public int getVertexIndex(int idx) { + return this.indices[idx]; + } + + /** + * Determines the orientation of the vertices in the quad. + */ + public static ModelQuadOrientation orient(float[] brightnesses) { + // If one side of the quad is brighter, flip the sides + if (brightnesses[0] + brightnesses[2] > brightnesses[1] + brightnesses[3]) { + return NORMAL; + } else { + return FLIP; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VanillaVertexTypes.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VanillaVertexTypes.java new file mode 100644 index 000000000..4d2ab7857 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VanillaVertexTypes.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.model.vertex; + +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.LineVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.LineVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.BasicScreenQuadVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.BasicScreenQuadVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + +public class VanillaVertexTypes { + public static final VanillaVertexType QUADS = new QuadVertexType(); + public static final VanillaVertexType LINES = new LineVertexType(); + public static final VanillaVertexType GLYPHS = new GlyphVertexType(); + public static final VanillaVertexType PARTICLES = new ParticleVertexType(); + public static final VanillaVertexType BASIC_SCREEN_QUADS = new BasicScreenQuadVertexType(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexDrain.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexDrain.java new file mode 100644 index 000000000..699beaf0b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexDrain.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.model.vertex; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.type.VertexType; + +/** + * A drain allows the instantiation of {@link VertexSink} and is implemented on outputs which take vertex data. + */ +public interface VertexDrain { + /** + * Returns a {@link VertexDrain} implementation on the provided {@link VertexConsumer}. Since the interface + * is always implemented on a given VertexConsumer, this is simply implemented as a cast internally. + * @param consumer The {@link VertexConsumer} + * @return A {@link VertexDrain} + */ + static VertexDrain of(VertexConsumer consumer) { + return (VertexDrain) consumer; + } + + /** + * Returns a {@link VertexSink} of type {@link T}, created from {@param factory}, which transforms and writes + * vertices through this vertex drain. + * + * @param factory The factory to create a vertex sink using + * @param The vertex sink's type + * @return A new {@link VertexSink} of type {@link T} + */ + T createSink(VertexType factory); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexSink.java new file mode 100644 index 000000000..4d932b365 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/VertexSink.java @@ -0,0 +1,36 @@ +package me.jellysquid.mods.sodium.client.model.vertex; + +/** + * Vertex sinks allow vertex data to be quickly written out to a {@link VertexDrain} while providing + * compile-time data format contracts. Generally, you will want a format-specific vertex sink, such as + * {@link me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink} in order to write + * vertex data. + */ +public interface VertexSink { + /** + * Ensures the backing storage to this sink has enough space for the given number of vertices to be written. This + * should be called with the number of vertices you expect to write before you make calls to write vertices. + * + * If the caller tries to write vertices without calling this method, or writes more vertices than they ensured + * there was capacity for, an {@link java.nio.BufferUnderflowException} may occur. + * + * When writing batches of vertices (such as those belonging to a primitive or a large model), it is best practice + * to simply call this method once at the start with the number of vertices you plan to write. This ensures the + * backing storage will only be resized once (if necessary) to fit the incoming vertex data. + * + * @param count The number of vertices + */ + void ensureCapacity(int count); + + /** + * Flushes any written vertex data to the {@link VertexDrain} this sink is connected to, ensuring it is actually + * written to the backing storage. This should be called after vertex data has been written to this sink. + * + * It is valid to flush a sink at any time. Only vertices that have been written since the last flush will be + * flushed when calling this method. If no vertices need to be flushed, this method does nothing. + * + * For optimal performance, callers should wait until they have written out as much vertex data as possible before + * flushing, in effect batching their writes. + */ + void flush(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferBuilder.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferBuilder.java new file mode 100644 index 000000000..d1bd26fec --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferBuilder.java @@ -0,0 +1,98 @@ +package me.jellysquid.mods.sodium.client.model.vertex.buffer; + +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; + +public class VertexBufferBuilder implements VertexBufferView { + private final BufferVertexFormat vertexFormat; + + private ByteBuffer buffer; + private int writerOffset; + private int capacity; + + public VertexBufferBuilder(BufferVertexFormat vertexFormat, int initialCapacity) { + this.vertexFormat = vertexFormat; + + this.buffer = BufferUtils.createByteBuffer(initialCapacity); + this.capacity = initialCapacity; + this.writerOffset = 0; + } + + private void grow(int len) { + // The new capacity will at least as large as the write it needs to service + int cap = Math.max(this.capacity * 2, this.capacity + len); + + // Allocate a new buffer and copy the old buffer's contents into it + ByteBuffer buffer = BufferUtils.createByteBuffer(cap); + this.buffer.rewind(); + buffer.put(this.buffer); + buffer.position(0); + + // Update the buffer and capacity now + this.buffer = buffer; + this.capacity = cap; + } + + @Override + public boolean ensureBufferCapacity(int bytes) { + if (this.writerOffset + bytes <= this.capacity) { + return false; + } + + this.grow(bytes); + + return true; + } + + @Override + public ByteBuffer getDirectBuffer() { + return this.buffer; + } + + @Override + public int getWriterPosition() { + return this.writerOffset; + } + + @Override + public void flush(int vertexCount, BufferVertexFormat format) { + if (this.vertexFormat != format) { + throw new IllegalStateException("Mis-matched vertex format (expected: [" + format + "], currently using: [" + this.vertexFormat + "])"); + } + + this.writerOffset += vertexCount * format.getStride(); + } + + @Override + public BufferVertexFormat getVertexFormat() { + return this.vertexFormat; + } + + public boolean isEmpty() { + return this.writerOffset == 0; + } + + public int getSize() { + return this.writerOffset; + } + + /** + * Ends the stream of written data and makes a copy of it to be passed around. + */ + public void copyInto(ByteBuffer dst) { + // Mark the slice of memory that needs to be copied + this.buffer.position(0); + this.buffer.limit(this.writerOffset); + + // Allocate a new buffer which is just large enough to contain the slice of vertex data + // The buffer is then flipped after the operation so the callee sees a range of bytes from (0,len] which can + // then be immediately passed to native libraries or the graphics driver + dst.put(this.buffer.slice()); + + // Reset the position and limit set earlier of the backing scratch buffer + this.buffer.clear(); + this.writerOffset = 0; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferView.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferView.java new file mode 100644 index 000000000..0ae537fc1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferView.java @@ -0,0 +1,43 @@ +package me.jellysquid.mods.sodium.client.model.vertex.buffer; + +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; + +import java.nio.ByteBuffer; + +/** + * Provides a view into {@link net.minecraft.client.render.BufferBuilder} and similar types. + */ +public interface VertexBufferView { + /** + * Ensures there is capacity in the buffer for the given number of bytes. + * @param bytes The number of bytes to allocate space for + * @return True if the buffer was resized, otherwise false + */ + boolean ensureBufferCapacity(int bytes); + + /** + * Returns a handle to the internal storage of this buffer. The buffer can be directly written into at the + * base address provided by {@link VertexBufferView#getWriterPosition()}. + * + * @return A {@link ByteBuffer} in off-heap space + */ + ByteBuffer getDirectBuffer(); + + /** + * @return The position at which new data should be written to, in bytes + */ + int getWriterPosition(); + + /** + * Flushes the given number of vertices to this buffer. This ensures that all constraints are still valid, and if + * so, advances the vertex counter and writer pointer to the end of the data that was written by the caller. + * @param vertexCount The number of vertices to flush + * @param format The format of each vertex + */ + void flush(int vertexCount, BufferVertexFormat format); + + /** + * @return The current vertex format of the buffer + */ + BufferVertexFormat getVertexFormat(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriter.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriter.java new file mode 100644 index 000000000..48e7727c4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriter.java @@ -0,0 +1,53 @@ +package me.jellysquid.mods.sodium.client.model.vertex.buffer; + +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.type.BufferVertexType; + +/** + * Base implementation of a {@link VertexSink} which writes into a {@link VertexBufferView} directly. + */ +public abstract class VertexBufferWriter implements VertexSink { + protected final VertexBufferView backingBuffer; + + protected final BufferVertexFormat vertexFormat; + protected final int vertexStride; + + private int vertexCount; + + protected VertexBufferWriter(VertexBufferView backingBuffer, BufferVertexType vertexType) { + this.backingBuffer = backingBuffer; + + this.vertexFormat = vertexType.getBufferVertexFormat(); + this.vertexStride = this.vertexFormat.getStride(); + + this.onBufferStorageChanged(); + } + + @Override + public void ensureCapacity(int count) { + if (this.backingBuffer.ensureBufferCapacity((this.vertexCount + count) * this.vertexStride)) { + this.onBufferStorageChanged(); + } + } + + @Override + public void flush() { + this.backingBuffer.flush(this.vertexCount, this.vertexFormat); + this.vertexCount = 0; + } + + /** + * Advances the write pointer forward by the stride of one vertex. This should always be called after a + * vertex is written. Implementations which override this should always call invoke the super implementation. + */ + protected void advance() { + this.vertexCount++; + } + + /** + * Called when the underlying memory buffer to the backing storage changes. When this is called, the implementation + * should update any pointers + */ + protected abstract void onBufferStorageChanged(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterNio.java new file mode 100644 index 000000000..c444c4714 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterNio.java @@ -0,0 +1,32 @@ +package me.jellysquid.mods.sodium.client.model.vertex.buffer; + +import me.jellysquid.mods.sodium.client.model.vertex.type.BufferVertexType; + +import java.nio.ByteBuffer; + +/** + * A safe {@link VertexBufferWriter} implementation which uses Java's NIO library to write into memory buffers. All + * write operations are checked and will throw an exception if an invalid memory access is detected. Supported on all + * platforms. + */ +public abstract class VertexBufferWriterNio extends VertexBufferWriter { + protected ByteBuffer byteBuffer; + protected int writeOffset; + + protected VertexBufferWriterNio(VertexBufferView backingBuffer, BufferVertexType vertexType) { + super(backingBuffer, vertexType); + } + + @Override + protected void onBufferStorageChanged() { + this.byteBuffer = this.backingBuffer.getDirectBuffer(); + this.writeOffset = this.backingBuffer.getWriterPosition(); + } + + @Override + protected void advance() { + this.writeOffset += this.vertexStride; + + super.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterUnsafe.java new file mode 100644 index 000000000..90994d7b2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/buffer/VertexBufferWriterUnsafe.java @@ -0,0 +1,34 @@ +package me.jellysquid.mods.sodium.client.model.vertex.buffer; + +import me.jellysquid.mods.sodium.client.model.vertex.type.BufferVertexType; +import org.lwjgl.MemoryUtil; + +/** + * An unsafe {@link VertexBufferWriter} implementation which uses direct memory operations to enable fast blitting of + * data into memory buffers. Only available on JVMs which support {@link sun.misc.Unsafe}, but generally produces much + * better optimized code than other implementations. The implementation does not check for invalid memory accesses, + * meaning that errors can corrupt process memory. + */ +public abstract class VertexBufferWriterUnsafe extends VertexBufferWriter { + /** + * The write pointer into the buffer storage. This is advanced by the vertex stride every time + * {@link VertexBufferWriterUnsafe#advance()} is called. + */ + protected long writePointer; + + protected VertexBufferWriterUnsafe(VertexBufferView backingBuffer, BufferVertexType vertexType) { + super(backingBuffer, vertexType); + } + + @Override + protected void onBufferStorageChanged() { + this.writePointer = MemoryUtil.getAddress(this.backingBuffer.getDirectBuffer(), this.backingBuffer.getWriterPosition()); + } + + @Override + protected void advance() { + this.writePointer += this.vertexStride; + + super.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/fallback/VertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/fallback/VertexWriterFallback.java new file mode 100644 index 000000000..c4e94727c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/fallback/VertexWriterFallback.java @@ -0,0 +1,31 @@ +package me.jellysquid.mods.sodium.client.model.vertex.fallback; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + + +/** + * The base implementation for a {@link VertexSink} which writes to a black-boxed {@link VertexConsumer}. This is the + * fallback path used when direct-writing optimizations cannot be used because the drain has no accessible backing + * memory. This implementation is very slow and should be avoided where possible. + * + * This sink does not support explicit batching/flushing and as such, all written vertices are immediately flushed + * to the backing implementation. + */ +public abstract class VertexWriterFallback implements VertexSink { + protected final VertexConsumer consumer; + + protected VertexWriterFallback(VertexConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void ensureCapacity(int count) { + // NO-OP + } + + @Override + public void flush() { + // NO-OP + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexSink.java new file mode 100644 index 000000000..99f6dc181 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexSink.java @@ -0,0 +1,41 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.glyph; + +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.util.math.Matrix4fExtended; +import me.jellysquid.mods.sodium.client.util.math.MatrixUtil; +import org.joml.Matrix4f; + +public interface GlyphVertexSink extends VertexSink { + VertexFormat VERTEX_FORMAT = DefaultVertexFormat.POSITION_TEXTURE_COLOR_LIGHT; + + /** + * Writes a glyph vertex to the sink. + * + * @param matrix The transformation matrix to apply to the vertex's position + * @see GlyphVertexSink#writeGlyph(float, float, float, int, float, float, int) + */ + default void writeGlyph(Matrix4f matrix, float x, float y, float z, int color, float u, float v, int light) { + Matrix4fExtended matrixExt = MatrixUtil.getExtendedMatrix(matrix); + + float x2 = matrixExt.transformVecX(x, y, z); + float y2 = matrixExt.transformVecY(x, y, z); + float z2 = matrixExt.transformVecZ(x, y, z); + + this.writeGlyph(x2, y2, z2, color, u, v, light); + } + + /** + * Writes a glyph vertex to the sink. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param color The ABGR-packed color of the vertex + * @param u The u-texture of the vertex + * @param v The v-texture of the vertex + * @param light The packed light map texture coordinates of the vertex + */ + void writeGlyph(float x, float y, float z, int color, float u, float v, int light); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexType.java new file mode 100644 index 000000000..8e51e6c7b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/GlyphVertexType.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.glyph; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer.GlyphVertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer.GlyphVertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer.GlyphVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + + +public class GlyphVertexType implements VanillaVertexType, BlittableVertexType { + @Override + public GlyphVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new GlyphVertexBufferWriterUnsafe(buffer) : new GlyphVertexBufferWriterNio(buffer); + } + + @Override + public GlyphVertexSink createFallbackWriter(VertexConsumer consumer) { + return new GlyphVertexWriterFallback(consumer); + } + + @Override + public VertexFormat getVertexFormat() { + return GlyphVertexSink.VERTEX_FORMAT; + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterNio.java new file mode 100644 index 000000000..ab8384b3a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterNio.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer; + +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; + +import java.nio.ByteBuffer; + +public class GlyphVertexBufferWriterNio extends VertexBufferWriterNio implements GlyphVertexSink { + public GlyphVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.GLYPHS); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + int i = this.writeOffset; + + ByteBuffer buffer = this.byteBuffer; + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putInt(i + 12, color); + buffer.putFloat(i + 16, u); + buffer.putFloat(i + 20, v); + buffer.putInt(i + 24, light); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..697a6a95e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexBufferWriterUnsafe.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; + +public class GlyphVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements GlyphVertexSink { + public GlyphVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.GLYPHS); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + CompatMemoryUtil.memPutFloat(i + 16, u); + CompatMemoryUtil.memPutFloat(i + 20, v); + CompatMemoryUtil.memPutInt(i + 24, light); + + this.advance(); + + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexWriterFallback.java new file mode 100644 index 000000000..56ec13a2a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/glyph/writer/GlyphVertexWriterFallback.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.fallback.VertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + + +public class GlyphVertexWriterFallback extends VertexWriterFallback implements GlyphVertexSink { + public GlyphVertexWriterFallback(VertexConsumer consumer) { + super(consumer); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + VertexConsumer consumer = this.consumer; + consumer.vertex(x, y, z); + consumer.color(ColorABGR.unpackRed(color), ColorABGR.unpackGreen(color), ColorABGR.unpackBlue(color), ColorABGR.unpackAlpha(color)); + consumer.texture(u, v); + consumer.light(light); + consumer.next(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexSink.java new file mode 100644 index 000000000..f0170431c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexSink.java @@ -0,0 +1,37 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.line; + +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + +public interface LineVertexSink extends VertexSink { + VertexFormat VERTEX_FORMAT = DefaultVertexFormat.POSITION_COLOR; + + /** + * Writes a line vertex to the sink. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param color The ABGR-packed color of the vertex + */ + void vertexLine(float x, float y, float z, int color); + + /** + * Writes a line vertex to the sink using unpacked normalized colors. This is slower than + * {@link LineVertexSink#vertexLine(float, float, float, int)} as it needs to pack the colors + * each call. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param r The normalized red component of the vertex's color + * @param g The normalized green component of the vertex's color + * @param b The normalized blue component of the vertex's color + * @param a The normalized alpha component of the vertex's color + */ + default void vertexLine(float x, float y, float z, float r, float g, float b, float a) { + this.vertexLine(x, y, z, ColorABGR.pack(r, g, b, a)); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexType.java new file mode 100644 index 000000000..eca743691 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/LineVertexType.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.line; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer.LineVertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer.LineVertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer.LineVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + + +public class LineVertexType implements VanillaVertexType, BlittableVertexType { + @Override + public LineVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new LineVertexBufferWriterUnsafe(buffer) : new LineVertexBufferWriterNio(buffer); + } + + @Override + public LineVertexSink createFallbackWriter(VertexConsumer consumer) { + return new LineVertexWriterFallback(consumer); + } + + @Override + public VertexFormat getVertexFormat() { + return LineVertexSink.VERTEX_FORMAT; + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterNio.java new file mode 100644 index 000000000..ab7c89a1e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterNio.java @@ -0,0 +1,27 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer; + +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.LineVertexSink; + +import java.nio.ByteBuffer; + +public class LineVertexBufferWriterNio extends VertexBufferWriterNio implements LineVertexSink { + public LineVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.LINES); + } + + @Override + public void vertexLine(float x, float y, float z, int color) { + int i = this.writeOffset; + + ByteBuffer buffer = this.byteBuffer; + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putInt(i + 12, color); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..2744be84a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexBufferWriterUnsafe.java @@ -0,0 +1,25 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.LineVertexSink; + +public class LineVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements LineVertexSink { + public LineVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.LINES); + } + + @Override + public void vertexLine(float x, float y, float z, int color) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexWriterFallback.java new file mode 100644 index 000000000..83ea76e0a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/line/writer/LineVertexWriterFallback.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.line.writer; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.fallback.VertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.formats.line.LineVertexSink; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + + +public class LineVertexWriterFallback extends VertexWriterFallback implements LineVertexSink { + public LineVertexWriterFallback(VertexConsumer consumer) { + super(consumer); + } + + @Override + public void vertexLine(float x, float y, float z, int color) { + VertexConsumer consumer = this.consumer; + consumer.vertex(x, y, z); + consumer.color(ColorABGR.unpackRed(color), ColorABGR.unpackGreen(color), ColorABGR.unpackBlue(color), ColorABGR.unpackAlpha(color)); + consumer.next(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexSink.java new file mode 100644 index 000000000..3e9b68465 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexSink.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.particle; + +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + + +public interface ParticleVertexSink extends VertexSink { + VertexFormat VERTEX_FORMAT = DefaultVertexFormat.POSITION_TEXTURE_COLOR_LIGHT; + + /** + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param u The u-texture of the vertex + * @param v The v-texture of the vertex + * @param color The ABGR-packed color of the vertex + * @param light The packed light map texture coordinates of the vertex + */ + void writeParticle(float x, float y, float z, float u, float v, int color, int light); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexType.java new file mode 100644 index 000000000..b333f1534 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/ParticleVertexType.java @@ -0,0 +1,32 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.particle; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer.ParticleVertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer.ParticleVertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer.ParticleVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + +public class ParticleVertexType implements VanillaVertexType, BlittableVertexType { + @Override + public ParticleVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new ParticleVertexBufferWriterUnsafe(buffer) : new ParticleVertexBufferWriterNio(buffer); + } + + @Override + public ParticleVertexSink createFallbackWriter(VertexConsumer consumer) { + return new ParticleVertexWriterFallback(consumer); + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } + + @Override + public VertexFormat getVertexFormat() { + return ParticleVertexSink.VERTEX_FORMAT; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterNio.java new file mode 100644 index 000000000..72a6db372 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterNio.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer; + +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexSink; + +import java.nio.ByteBuffer; + +public class ParticleVertexBufferWriterNio extends VertexBufferWriterNio implements ParticleVertexSink { + public ParticleVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.PARTICLES); + } + + @Override + public void writeParticle(float x, float y, float z, float u, float v, int color, int light) { + int i = this.writeOffset; + + ByteBuffer buffer = this.byteBuffer; + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putFloat(i + 12, u); + buffer.putFloat(i + 16, v); + buffer.putInt(i + 20, color); + buffer.putInt(i + 24, light); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..564f18bc2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexBufferWriterUnsafe.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexSink; + +public class ParticleVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements ParticleVertexSink { + public ParticleVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.PARTICLES); + } + + @Override + public void writeParticle(float x, float y, float z, float u, float v, int color, int light) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutFloat(i + 12, u); + CompatMemoryUtil.memPutFloat(i + 16, v); + CompatMemoryUtil.memPutInt(i + 20, color); + CompatMemoryUtil.memPutInt(i + 24, light); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexWriterFallback.java new file mode 100644 index 000000000..58fc32a67 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/particle/writer/ParticleVertexWriterFallback.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.particle.writer; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.fallback.VertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexSink; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + + +public class ParticleVertexWriterFallback extends VertexWriterFallback implements ParticleVertexSink { + public ParticleVertexWriterFallback(VertexConsumer consumer) { + super(consumer); + } + + @Override + public void writeParticle(float x, float y, float z, float u, float v, int color, int light) { + VertexConsumer consumer = this.consumer; + consumer.vertex(x, y, z); + consumer.texture(u, v); + consumer.color(ColorABGR.unpackRed(color), ColorABGR.unpackGreen(color), ColorABGR.unpackBlue(color), ColorABGR.unpackAlpha(color)); + consumer.light(light); + consumer.next(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexSink.java new file mode 100644 index 000000000..f34a41d1a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexSink.java @@ -0,0 +1,45 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.quad; + +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.util.math.Matrix4fExtended; +import me.jellysquid.mods.sodium.client.util.math.MatrixUtil; + + +public interface QuadVertexSink extends VertexSink { + VertexFormat VERTEX_FORMAT = DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL; + + /** + * Writes a quad vertex to this sink. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param color The ABGR-packed color of the vertex + * @param u The u-texture of the vertex + * @param v The y-texture of the vertex + * @param light The packed light-map coordinates of the vertex + * @param overlay The packed overlay-map coordinates of the vertex + * @param normal The 3-byte packed normal vector of the vertex + */ + void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal); + + /** + * Writes a quad vertex to the sink, transformed by the given matrices. + * + * @param matrices The matrices to transform the vertex's position and normal vectors by + */ + default void writeQuad(MatrixStack.Entry matrices, float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + Matrix4fExtended modelMatrix = MatrixUtil.getExtendedMatrix(matrices.getModel()); + + float x2 = modelMatrix.transformVecX(x, y, z); + float y2 = modelMatrix.transformVecY(x, y, z); + float z2 = modelMatrix.transformVecZ(x, y, z); + + int norm = MatrixUtil.transformPackedNormal(normal, matrices.getNormal()); + + this.writeQuad(x2, y2, z2, color, u, v, light, overlay, norm); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexType.java new file mode 100644 index 000000000..eae76e6ff --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/QuadVertexType.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.quad; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer.QuadVertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer.QuadVertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer.QuadVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + + +public class QuadVertexType implements VanillaVertexType, BlittableVertexType { + @Override + public QuadVertexSink createFallbackWriter(VertexConsumer consumer) { + return new QuadVertexWriterFallback(consumer); + } + + @Override + public QuadVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new QuadVertexBufferWriterUnsafe(buffer) : new QuadVertexBufferWriterNio(buffer); + } + + @Override + public VertexFormat getVertexFormat() { + return QuadVertexSink.VERTEX_FORMAT; + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterNio.java new file mode 100644 index 000000000..ef26488a4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterNio.java @@ -0,0 +1,32 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer; + +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; + +import java.nio.ByteBuffer; + +public class QuadVertexBufferWriterNio extends VertexBufferWriterNio implements QuadVertexSink { + public QuadVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.QUADS); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + int i = this.writeOffset; + + ByteBuffer buf = this.byteBuffer; + buf.putFloat(i, x); + buf.putFloat(i + 4, y); + buf.putFloat(i + 8, z); + buf.putInt(i + 12, color); + buf.putFloat(i + 16, u); + buf.putFloat(i + 20, v); + buf.putInt(i + 24, overlay); + buf.putInt(i + 28, light); + buf.putInt(i + 32, normal); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..641b9bb71 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexBufferWriterUnsafe.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; + +public class QuadVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements QuadVertexSink { + public QuadVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.QUADS); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + CompatMemoryUtil.memPutFloat(i + 16, u); + CompatMemoryUtil.memPutFloat(i + 20, v); + CompatMemoryUtil.memPutInt(i + 24, overlay); + CompatMemoryUtil.memPutInt(i + 28, light); + CompatMemoryUtil.memPutInt(i + 32, normal); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexWriterFallback.java new file mode 100644 index 000000000..eafc795ba --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/quad/writer/QuadVertexWriterFallback.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.fallback.VertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + + +public class QuadVertexWriterFallback extends VertexWriterFallback implements QuadVertexSink { + public QuadVertexWriterFallback(VertexConsumer consumer) { + super(consumer); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + VertexConsumer consumer = this.consumer; + consumer.vertex(x, y, z); + consumer.color(ColorABGR.unpackRed(color), ColorABGR.unpackGreen(color), ColorABGR.unpackBlue(color), ColorABGR.unpackAlpha(color)); + consumer.texture(u, v); + consumer.overlay(overlay); + consumer.light(light); + consumer.normal(Norm3b.unpackX(normal), Norm3b.unpackY(normal), Norm3b.unpackZ(normal)); + consumer.next(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexSink.java new file mode 100644 index 000000000..84bea99f8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexSink.java @@ -0,0 +1,37 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad; + +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.util.math.Matrix4fExtended; +import me.jellysquid.mods.sodium.client.util.math.MatrixUtil; +import org.joml.Matrix4f; + +public interface BasicScreenQuadVertexSink extends VertexSink { + VertexFormat VERTEX_FORMAT = DefaultVertexFormat.POSITION_COLOR; + + /** + * Writes a quad vertex to this sink. + * + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param color The ABGR-packed color of the vertex + */ + void writeQuad(float x, float y, float z, int color); + + /** + * Writes a quad vertex to the sink, transformed by the given matrix. + * + * @param matrix The matrix to transform the vertex's position by + */ + default void writeQuad(Matrix4f matrix, float x, float y, float z, int color) { + Matrix4fExtended modelMatrix = MatrixUtil.getExtendedMatrix(matrix); + + float x2 = modelMatrix.transformVecX(x, y, z); + float y2 = modelMatrix.transformVecY(x, y, z); + float z2 = modelMatrix.transformVecZ(x, y, z); + + this.writeQuad(x2, y2, z2, color); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexType.java new file mode 100644 index 000000000..1b2527ccc --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/BasicScreenQuadVertexType.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer.BasicScreenQuadVertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer.BasicScreenQuadVertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer.BasicScreenQuadVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; + + +public class BasicScreenQuadVertexType implements VanillaVertexType, BlittableVertexType { + @Override + public BasicScreenQuadVertexSink createFallbackWriter(VertexConsumer consumer) { + return new BasicScreenQuadVertexWriterFallback(consumer); + } + + @Override + public BasicScreenQuadVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new BasicScreenQuadVertexBufferWriterUnsafe(buffer) : new BasicScreenQuadVertexBufferWriterNio(buffer); + } + + @Override + public VertexFormat getVertexFormat() { + return BasicScreenQuadVertexSink.VERTEX_FORMAT; + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterNio.java new file mode 100644 index 000000000..9de639cb2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterNio.java @@ -0,0 +1,27 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer; + +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.BasicScreenQuadVertexSink; + +import java.nio.ByteBuffer; + +public class BasicScreenQuadVertexBufferWriterNio extends VertexBufferWriterNio implements BasicScreenQuadVertexSink { + public BasicScreenQuadVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.BASIC_SCREEN_QUADS); + } + + @Override + public void writeQuad(float x, float y, float z, int color) { + int i = this.writeOffset; + + ByteBuffer buf = this.byteBuffer; + buf.putFloat(i, x); + buf.putFloat(i + 4, y); + buf.putFloat(i + 8, z); + buf.putInt(i + 12, color); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..502dbb226 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexBufferWriterUnsafe.java @@ -0,0 +1,25 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.VanillaVertexTypes; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.BasicScreenQuadVertexSink; + +public class BasicScreenQuadVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements BasicScreenQuadVertexSink { + public BasicScreenQuadVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, VanillaVertexTypes.BASIC_SCREEN_QUADS); + } + + @Override + public void writeQuad(float x, float y, float z, int color) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexWriterFallback.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexWriterFallback.java new file mode 100644 index 000000000..5a1922e12 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/formats/screen_quad/writer/BasicScreenQuadVertexWriterFallback.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.writer; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.fallback.VertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.formats.screen_quad.BasicScreenQuadVertexSink; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; + + +public class BasicScreenQuadVertexWriterFallback extends VertexWriterFallback implements BasicScreenQuadVertexSink { + public BasicScreenQuadVertexWriterFallback(VertexConsumer consumer) { + super(consumer); + } + + @Override + public void writeQuad(float x, float y, float z, int color) { + VertexConsumer consumer = this.consumer; + consumer.vertex(x, y, z); + consumer.color(ColorABGR.unpackRed(color), ColorABGR.unpackGreen(color), ColorABGR.unpackBlue(color), ColorABGR.unpackAlpha(color)); + consumer.next(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/AbstractVertexTransformer.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/AbstractVertexTransformer.java new file mode 100644 index 000000000..bb616beab --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/AbstractVertexTransformer.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.model.vertex.transformers; + +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +/** + * A vertex transformer wraps an {@link VertexSink} interface to modify incoming vertex data, delegating any + * actual logic to the inner sink. + * @param The {@link VertexSink} interface this transformer wraps + */ +public abstract class AbstractVertexTransformer implements VertexSink { + protected final T delegate; + + protected AbstractVertexTransformer(T delegate) { + this.delegate = delegate; + } + + @Override + public void ensureCapacity(int count) { + this.delegate.ensureCapacity(count); + } + + @Override + public void flush() { + this.delegate.flush(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/SpriteTexturedVertexTransformer.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/SpriteTexturedVertexTransformer.java new file mode 100644 index 000000000..2ed30abb8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/transformers/SpriteTexturedVertexTransformer.java @@ -0,0 +1,80 @@ +package me.jellysquid.mods.sodium.client.model.vertex.transformers; + +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.particle.ParticleVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +/** + * Base implementation for a {@link VertexSink} which transforms texture coordinates relative to a sprite's bounds. + * + * @param The {@link VertexSink} interface this transformer wraps + */ +public abstract class SpriteTexturedVertexTransformer extends AbstractVertexTransformer { + private final float uMin; + private final float vMin; + + private final float uMaxMin; + private final float vMaxMin; + + public SpriteTexturedVertexTransformer(T delegate, TextureAtlasSprite sprite) { + super(delegate); + + this.uMin = sprite.getMinU(); + this.vMin = sprite.getMinV(); + + this.uMaxMin = sprite.getMaxU() - this.uMin; + this.vMaxMin = sprite.getMaxV() - this.vMin; + } + + protected float transformTextureU(float u) { + return (this.uMaxMin * u) + this.uMin; + } + + protected float transformTextureV(float v) { + return (this.vMaxMin * v) + this.vMin; + } + + public static class Quad extends SpriteTexturedVertexTransformer implements QuadVertexSink { + public Quad(QuadVertexSink delegate, TextureAtlasSprite sprite) { + super(delegate, sprite); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + u = this.transformTextureU(u); + v = this.transformTextureV(v); + + this.delegate.writeQuad(x, y, z, color, u, v, light, overlay, normal); + } + } + + public static class Particle extends SpriteTexturedVertexTransformer implements ParticleVertexSink { + public Particle(ParticleVertexSink delegate, TextureAtlasSprite sprite) { + super(delegate, sprite); + } + + @Override + public void writeParticle(float x, float y, float z, float u, float v, int color, int light) { + u = this.transformTextureU(u); + v = this.transformTextureV(v); + + this.delegate.writeParticle(x, y, z, u, v, color, light); + } + } + + public static class Glyph extends SpriteTexturedVertexTransformer implements GlyphVertexSink { + public Glyph(GlyphVertexSink delegate, TextureAtlasSprite sprite) { + super(delegate, sprite); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + u = this.transformTextureU(u); + v = this.transformTextureV(v); + + this.delegate.writeGlyph(x, y, z, color, u, v, light); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BlittableVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BlittableVertexType.java new file mode 100644 index 000000000..8ac1a1977 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BlittableVertexType.java @@ -0,0 +1,15 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; + +public interface BlittableVertexType extends BufferVertexType { + /** + * Creates a {@link VertexSink} which writes into a {@link VertexBufferView}. This allows for specialization + * when the memory storage is known. + * + * @param buffer The backing vertex buffer + * @param direct True if direct memory access is allowed, otherwise false + */ + T createBufferWriter(VertexBufferView buffer, boolean direct); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BufferVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BufferVertexType.java new file mode 100644 index 000000000..2a4cdaa65 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/BufferVertexType.java @@ -0,0 +1,14 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +/** + * A blittable {@link VertexType} which supports direct copying into a {@link net.minecraft.client.render.BufferBuilder} + * provided the buffer's vertex format matches that required by the {@link VertexSink}. + * + * @param The {@link VertexSink} type this factory produces + */ +public interface BufferVertexType extends VertexType { + BufferVertexFormat getBufferVertexFormat(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/ChunkVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/ChunkVertexType.java new file mode 100644 index 000000000..ad7aaee1b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/ChunkVertexType.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public interface ChunkVertexType extends BlittableVertexType, CustomVertexType { + /** + * @return The scale to be applied to vertex coordinates + */ + float getModelScale(); + + /** + * @return The scale to be applied to texture coordinates + */ + float getTextureScale(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/CustomVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/CustomVertexType.java new file mode 100644 index 000000000..980c01015 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/CustomVertexType.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +public interface CustomVertexType> extends BufferVertexType { + /** + * @return The {@link GlVertexFormat} required for blitting (direct writing into buffers) + */ + GlVertexFormat getCustomVertexFormat(); + + @Override + default BufferVertexFormat getBufferVertexFormat() { + return this.getCustomVertexFormat(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VanillaVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VanillaVertexType.java new file mode 100644 index 000000000..e6b510ceb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VanillaVertexType.java @@ -0,0 +1,13 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +public interface VanillaVertexType extends BufferVertexType { + default BufferVertexFormat getBufferVertexFormat() { + return BufferVertexFormat.from(this.getVertexFormat()); + } + + VertexFormat getVertexFormat(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VertexType.java new file mode 100644 index 000000000..689c2a9ee --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/vertex/type/VertexType.java @@ -0,0 +1,27 @@ +package me.jellysquid.mods.sodium.client.model.vertex.type; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +/** + * Provides factories which create a {@link VertexSink} for the given vertex format. + * + * @param The {@link VertexSink} type this factory produces + */ +public interface VertexType { + /** + * Creates a {@link VertexSink} which can write into any {@link VertexConsumer}. This is generally used when + * a special implementation of {@link VertexConsumer} is used that cannot be optimized for, or when + * complex/unsupported transformations need to be performed using vanilla code paths. + * @param consumer The {@link VertexConsumer} to write into + */ + T createFallbackWriter(VertexConsumer consumer); + + /** + * If this vertex type supports {@link BufferVertexType}, then this method returns this vertex type as a + * blittable type, performing a safe cast. + */ + default BlittableVertexType asBlittable() { + return null; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java b/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java new file mode 100644 index 000000000..65535adf1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java @@ -0,0 +1,30 @@ +package me.jellysquid.mods.sodium.client.render; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.shadows.ShadowRenderingState; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; + +public class GameRendererContext { + /** + * Obtains a model-view-projection matrix by multiplying the projection matrix with the model-view matrix + * from {@param matrices}. + * + * @return A float-buffer on the stack containing the model-view-projection matrix in a format suitable for + * uploading as uniform state + */ + public static FloatBuffer getModelViewProjectionMatrix(MatrixStack.Entry matrices) { + final FloatBuffer bufModelViewProjection = BufferUtils.createFloatBuffer(16); + final Matrix4f projectionMatrix = (AngelicaConfig.enableIris && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) ? ShadowRenderingState.getShadowOrthoMatrix() : RenderingState.INSTANCE.getProjectionMatrix(); + final Matrix4f matrix = new Matrix4f(projectionMatrix); + matrix.mul(matrices.getModel()); + + matrix.get(bufModelViewProjection); + + return bufModelViewProjection; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java new file mode 100644 index 000000000..373c42dd5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java @@ -0,0 +1,496 @@ +package me.jellysquid.mods.sodium.client.render; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.mojang.ChunkPos; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import jss.notfine.core.SettingsManager; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderManager; +import me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw.MultidrawChunkRenderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.backends.oneshot.ChunkRenderBackendOneshot; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.DefaultModelVertexFormats; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheShared; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import me.jellysquid.mods.sodium.client.world.ChunkStatusListener; +import me.jellysquid.mods.sodium.common.util.ListUtil; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.pipeline.ShadowRenderer; +import net.coderbot.iris.shadows.ShadowRenderingState; +import net.coderbot.iris.sodium.shadow_map.SwappableChunkRenderManager; +import net.coderbot.iris.sodium.vertex_format.IrisModelVertexFormats; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.profiler.Profiler; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MathHelper; +import net.minecraftforge.client.MinecraftForgeClient; +import org.joml.Vector3d; + +import java.util.Set; + +/** + * Provides an extension to vanilla's {@link WorldRenderer}. + */ +public class SodiumWorldRenderer implements ChunkStatusListener { + private static SodiumWorldRenderer instance; + + private final Minecraft client; + + private WorldClient world; + private int renderDistance; + + private double lastCameraX, lastCameraY, lastCameraZ; + private double lastCameraPitch, lastCameraYaw; + + private boolean useEntityCulling; + + private final LongSet loadedChunkPositions = new LongOpenHashSet(); + private final Set globalTileEntities = new ObjectOpenHashSet<>(); + + + @Getter private Frustrum frustum; + private ChunkRenderManager chunkRenderManager; + private ChunkRenderBackend chunkRenderBackend; + + // Iris + private boolean wasRenderingShadows = false; + + private double iris$swapLastCameraX, iris$swapLastCameraY, iris$swapLastCameraZ, iris$swapLastCameraPitch, iris$swapLastCameraYaw; + /** + * Instantiates Sodium's world renderer. This should be called at the time of the world renderer initialization. + */ + public static SodiumWorldRenderer create(Minecraft mc) { + if (instance == null) { + instance = new SodiumWorldRenderer(mc); + } + + return instance; + } + + /** + * @throws IllegalStateException If the renderer has not yet been created + * @return The current instance of this type + */ + public static SodiumWorldRenderer getInstance() { + if (instance == null) { + throw new IllegalStateException("Renderer not initialized"); + } + + return instance; + } + + private SodiumWorldRenderer(Minecraft client) { + this.client = client; + } + + public void setWorld(WorldClient world) { + // Check that the world is actually changing + if (this.world == world) { + return; + } + + // If we have a world is already loaded, unload the renderer + if (this.world != null) { + this.unloadWorld(); + } + + // If we're loading a new world, load the renderer + if (world != null) { + this.loadWorld(world); + } + } + + public int getChunksSubmitted() { + return this.chunkRenderManager != null ? this.chunkRenderManager.getAndResetSubmitted() : 0; + } + + private void loadWorld(WorldClient world) { + this.world = world; + + ChunkRenderCacheShared.createRenderContext(this.world); + + this.initRenderer(); + } + + private void unloadWorld() { + ChunkRenderCacheShared.destroyRenderContext(this.world); + + if (this.chunkRenderManager != null) { + this.chunkRenderManager.destroy(); + this.chunkRenderManager = null; + } + + if (this.chunkRenderBackend != null) { + this.chunkRenderBackend.delete(); + this.chunkRenderBackend = null; + } + + this.loadedChunkPositions.clear(); + this.globalTileEntities.clear(); + + this.world = null; + } + + /** + * @return The number of chunk renders which are visible in the current camera's frustum + */ + public int getVisibleChunkCount() { + return this.chunkRenderManager.getVisibleChunkCount(); + } + + /** + * Notifies the chunk renderer that the graph scene has changed and should be re-computed. + */ + public void scheduleTerrainUpdate() { + if (this.chunkRenderManager != null) { + if(AngelicaConfig.enableIris) iris$ensureStateSwapped(); + this.chunkRenderManager.markDirty(); + } + } + + /** + * @return True if no chunks are pending rebuilds + */ + public boolean isTerrainRenderComplete() { + return this.chunkRenderManager.isBuildComplete(); + } + + // We'll keep it to have compatibility with Oculus' older versions + public static boolean hasChanges = false; + + /** + * Called prior to any chunk rendering in order to update necessary state. + */ + public void updateChunks(Camera camera, Frustrum frustum, boolean hasForcedFrustum, int frame, boolean spectator) { + this.frustum = frustum; + + this.useEntityCulling = SodiumClientMod.options().advanced.useEntityCulling; + + Profiler profiler = this.client.mcProfiler; + profiler.startSection("camera_setup"); + + EntityPlayer player = this.client.thePlayer; + + if (player == null) { + throw new IllegalStateException("Client instance has no active player entity"); + } + + Vector3d pos = camera.getPos(); + + this.chunkRenderManager.setCameraPosition(pos.x, pos.y, pos.z); + + float pitch = camera.getPitch(); + float yaw = camera.getYaw(); + + boolean dirty = pos.x != this.lastCameraX || pos.y != this.lastCameraY || pos.z != this.lastCameraZ || + pitch != this.lastCameraPitch || yaw != this.lastCameraYaw; + + if(AngelicaConfig.enableIris) { + iris$ensureStateSwapped(); + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + dirty = true; + } + } + if (dirty) { + this.chunkRenderManager.markDirty(); + } + + this.lastCameraX = pos.x; + this.lastCameraY = pos.y; + this.lastCameraZ = pos.z; + this.lastCameraPitch = pitch; + this.lastCameraYaw = yaw; + + profiler.endStartSection("chunk_update"); + + this.chunkRenderManager.updateChunks(); + + if (!hasForcedFrustum && this.chunkRenderManager.isDirty()) { + profiler.endStartSection("chunk_graph_rebuild"); + + this.chunkRenderManager.update(camera, (FrustumExtended) frustum, frame, spectator); + } + + profiler.endStartSection("visible_chunk_tick"); + + this.chunkRenderManager.tickVisibleRenders(); + + profiler.endSection(); + + SodiumGameOptions.EntityRenderDistance.setRenderDistanceMult(MathHelper.clamp_double((double) this.client.gameSettings.renderDistanceChunks / 8.0D, 1.0D, 2.5D) * (double) 1.0F * (SettingsManager.entityRenderScaleFactor)); + if(AngelicaConfig.enableIris) { + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + ShadowRenderer.visibleTileEntities.addAll(this.chunkRenderManager.getVisibleTileEntities()); + } + } + } + + /** + * Performs a render pass for the given {@link RenderLayer} and draws all visible chunks for it. + */ + public void drawChunkLayer(BlockRenderPass pass, MatrixStack matrixStack, double x, double y, double z) { + if(AngelicaConfig.enableIris) iris$ensureStateSwapped(); + // startDrawing/endDrawing are handled by 1.7 already + // pass.startDrawing(); + + this.chunkRenderManager.renderLayer(matrixStack, pass, x, y, z); + + //pass.endDrawing(); + GLStateManager.clearCurrentColor(); + } + + public void reload() { + if (this.world == null) { + return; + } + + this.initRenderer(); + } + + private void initRenderer() { + if (this.chunkRenderManager != null) { + this.chunkRenderManager.destroy(); + this.chunkRenderManager = null; + } + + if (this.chunkRenderBackend != null) { + this.chunkRenderBackend.delete(); + this.chunkRenderBackend = null; + } + + this.globalTileEntities.clear(); + + RenderDevice device = RenderDevice.INSTANCE; + + SodiumGameOptions opts = SodiumClientMod.options(); + + final ChunkVertexType vertexFormat; + + if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { + vertexFormat = IrisModelVertexFormats.MODEL_VERTEX_XHFP; + } else if (opts.advanced.useCompactVertexFormat) { + vertexFormat = DefaultModelVertexFormats.MODEL_VERTEX_HFP; + } else { + vertexFormat = DefaultModelVertexFormats.MODEL_VERTEX_SFP; + } + + this.chunkRenderBackend = createChunkRenderBackend(device, opts, vertexFormat); + this.chunkRenderBackend.createShaders(device); + + this.chunkRenderManager = new ChunkRenderManager<>(this, this.chunkRenderBackend, this.world, this.client.gameSettings.renderDistanceChunks); + this.chunkRenderManager.restoreChunks(this.loadedChunkPositions); + } + + private static ChunkRenderBackend createChunkRenderBackend(RenderDevice device, SodiumGameOptions options, ChunkVertexType vertexFormat) { + boolean disableBlacklist = SodiumClientMod.options().advanced.ignoreDriverBlacklist; + + if (options.advanced.useChunkMultidraw && MultidrawChunkRenderBackend.isSupported(disableBlacklist)) { + return new MultidrawChunkRenderBackend(device, vertexFormat); + } else { + return new ChunkRenderBackendOneshot(vertexFormat); + } + } + + private boolean checkBEVisibility(TileEntity entity) { + return frustum.isBoundingBoxInFrustum(entity.getRenderBoundingBox()); + } + + private void renderTE(TileEntity tileEntity, int pass, float partialTicks) { + if(!tileEntity.shouldRenderInPass(pass) || !checkBEVisibility(tileEntity)) + return; + + try { + TileEntityRendererDispatcher.instance.renderTileEntity(tileEntity, partialTicks); + } catch(RuntimeException e) { + if(tileEntity.isInvalid()) { + SodiumClientMod.logger().error("Suppressing crash from invalid tile entity", e); + } else { + throw e; + } + } + } + + public void renderTileEntities(EntityLivingBase entity, ICamera camera, float partialTicks) { + int pass = MinecraftForgeClient.getRenderPass(); + for (TileEntity tileEntity : this.chunkRenderManager.getVisibleTileEntities()) { + renderTE(tileEntity, pass, partialTicks); + } + + for (TileEntity tileEntity : this.globalTileEntities) { + renderTE(tileEntity, pass, partialTicks); + } + } + + @Override + public void onChunkAdded(int x, int z) { + this.loadedChunkPositions.add(ChunkPos.toLong(x, z)); + this.chunkRenderManager.onChunkAdded(x, z); + } + + @Override + public void onChunkRemoved(int x, int z) { + this.loadedChunkPositions.remove(ChunkPos.toLong(x, z)); + this.chunkRenderManager.onChunkRemoved(x, z); + } + + public void onChunkRenderUpdated(int x, int y, int z, ChunkRenderData meshBefore, ChunkRenderData meshAfter) { + ListUtil.updateList(this.globalTileEntities, meshBefore.getGlobalTileEntities(), meshAfter.getGlobalTileEntities()); + + this.chunkRenderManager.onChunkRenderUpdates(x, y, z, meshAfter); + } + + private static boolean isInfiniteExtentsBox(AxisAlignedBB box) { + return Double.isInfinite(box.minX) || Double.isInfinite(box.minY) || Double.isInfinite(box.minZ) + || Double.isInfinite(box.maxX) || Double.isInfinite(box.maxY) || Double.isInfinite(box.maxZ); + } + + /** + * Returns whether or not the entity intersects with any visible chunks in the graph. + * @return True if the entity is visible, otherwise false + */ + public boolean isEntityVisible(Entity entity) { + if (!this.useEntityCulling || entity.ignoreFrustumCheck) { + return true; + } + + AxisAlignedBB box = entity.boundingBox; + + // Entities outside the valid world height will never map to a rendered chunk + // Always render these entities or they'll be culled incorrectly! + if (box.maxY < 0.5D || box.minY > 255.5D) { + return true; + } + + if (isInfiniteExtentsBox(box)) { + return true; + } + + // Ensure entities with outlines or nametags are always visible + // TODO: Sodium - Outlines + if (/*this.client.hasOutline(entity) || */ (entity instanceof EntityLiving living && living.hasCustomNameTag())) { + return true; + } + + int minX = MathHelper.floor_double(box.minX - 0.5D) >> 4; + int minY = MathHelper.floor_double(box.minY - 0.5D) >> 4; + int minZ = MathHelper.floor_double(box.minZ - 0.5D) >> 4; + + int maxX = MathHelper.floor_double(box.maxX + 0.5D) >> 4; + int maxY = MathHelper.floor_double(box.maxY + 0.5D) >> 4; + int maxZ = MathHelper.floor_double(box.maxZ + 0.5D) >> 4; + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + if (this.chunkRenderManager.isChunkVisible(x, y, z)) { + return true; + } + } + } + } + + return false; + } + + public String getChunksDebugString() { + // C: visible/total + // TODO: add dirty and queued counts + return String.format("C: %s/%s S: %s Q: %s+%si ", this.chunkRenderManager.getVisibleChunkCount(), this.chunkRenderManager.getTotalSections(), this.chunkRenderManager.getSubmitted(), this.chunkRenderManager.getRebuildQueueSize(), this.chunkRenderManager.getImportantRebuildQueueSize()); + } + + /** + * Schedules chunk rebuilds for all chunks in the specified block region. + */ + public void scheduleRebuildForBlockArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { + this.scheduleRebuildForChunks(minX >> 4, minY >> 4, minZ >> 4, maxX >> 4, maxY >> 4, maxZ >> 4, important); + } + + /** + * Schedules chunk rebuilds for all chunks in the specified chunk region. + */ + public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { + for (int chunkX = minX; chunkX <= maxX; chunkX++) { + for (int chunkY = minY; chunkY <= maxY; chunkY++) { + for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) { + this.scheduleRebuildForChunk(chunkX, chunkY, chunkZ, important); + } + } + } + } + + /** + * Schedules a chunk rebuild for the render belonging to the given chunk section position. + */ + public void scheduleRebuildForChunk(int x, int y, int z, boolean important) { + this.chunkRenderManager.scheduleRebuild(x, y, z, important); + } + + public ChunkRenderBackend getChunkRenderer() { + return this.chunkRenderBackend; + } + + // Iris + private void swapCachedCameraPositions() { + double tmp; + + tmp = lastCameraX; + lastCameraX = iris$swapLastCameraX; + iris$swapLastCameraX = tmp; + + tmp = lastCameraY; + lastCameraY = iris$swapLastCameraY; + iris$swapLastCameraY = tmp; + + tmp = lastCameraZ; + lastCameraZ = iris$swapLastCameraZ; + iris$swapLastCameraZ = tmp; + + tmp = lastCameraPitch; + lastCameraPitch = iris$swapLastCameraPitch; + iris$swapLastCameraPitch = tmp; + + tmp = lastCameraYaw; + lastCameraYaw = iris$swapLastCameraYaw; + iris$swapLastCameraYaw = tmp; + } + + private void iris$ensureStateSwapped() { + if (!wasRenderingShadows && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + if (this.chunkRenderManager instanceof SwappableChunkRenderManager) { + ((SwappableChunkRenderManager) this.chunkRenderManager).iris$swapVisibilityState(); + swapCachedCameraPositions(); + } + + wasRenderingShadows = true; + } else if (wasRenderingShadows && !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + if (this.chunkRenderManager instanceof SwappableChunkRenderManager) { + ((SwappableChunkRenderManager) this.chunkRenderManager).iris$swapVisibilityState(); + swapCachedCameraPositions(); + } + + wasRenderingShadows = false; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkCameraContext.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkCameraContext.java new file mode 100644 index 000000000..3610c382e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkCameraContext.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +public class ChunkCameraContext { + public final int blockOriginX, blockOriginY, blockOriginZ; + public final float originX, originY, originZ; + + public ChunkCameraContext(double x, double y, double z) { + this.blockOriginX = (int) x; + this.blockOriginY = (int) y; + this.blockOriginZ = (int) z; + + this.originX = (float) (x - this.blockOriginX); + this.originY = (float) (y - this.blockOriginY); + this.originZ = (float) (z - this.blockOriginZ); + } + + public float getChunkModelOffset(int chunkBlockPos, int cameraBlockPos, float cameraPos) { + int t = chunkBlockPos - cameraBlockPos; + return t - cameraPos; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkGraphicsState.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkGraphicsState.java new file mode 100644 index 000000000..0c74e0c4b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkGraphicsState.java @@ -0,0 +1,39 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +import me.jellysquid.mods.sodium.client.gl.device.CommandList; + +import java.nio.ByteBuffer; + +public abstract class ChunkGraphicsState { + private final int x, y, z; + + private ByteBuffer translucencyData; + + protected ChunkGraphicsState(ChunkRenderContainer container) { + this.x = container.getRenderX(); + this.y = container.getRenderY(); + this.z = container.getRenderZ(); + } + + public abstract void delete(CommandList commandList); + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + + public ByteBuffer getTranslucencyData() { + return this.translucencyData; + } + + public void setTranslucencyData(ByteBuffer data) { + this.translucencyData = data; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderBackend.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderBackend.java new file mode 100644 index 000000000..f45907e31 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderBackend.java @@ -0,0 +1,62 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.lists.ChunkRenderListIterator; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * The chunk render backend takes care of managing the graphics resource state of chunk render containers. This includes + * the handling of uploading their data to the graphics card and rendering responsibilities. + * @param The type of graphics state to be used in chunk render containers + */ +public interface ChunkRenderBackend { + /** + * Drains the iterator of items and processes each build task's result serially. After this method returns, all + * drained results should be processed. + */ + void upload(CommandList commandList, Iterator> queue); + + /** + * Renders the given chunk render list to the active framebuffer. + * @param commandList The command list which OpenGL commands should be serialized to + * @param renders An iterator over the list of chunks to be rendered + * @param camera The camera context containing chunk offsets for the current render + */ + void render(CommandList commandList, ChunkRenderListIterator renders, ChunkCameraContext camera); + + void createShaders(RenderDevice device); + + void begin(MatrixStack matrixStack); + + void end(MatrixStack matrixStack); + + /** + * Deletes this render backend and any resources attached to it. + */ + void delete(); + + /** + * Returns the vertex format used by this chunk render backend for rendering meshes. + */ + ChunkVertexType getVertexType(); + + Class getGraphicsStateType(); + + default String getRendererName() { + return this.getClass().getSimpleName(); + } + + default List getDebugStrings() { + return Collections.emptyList(); + } + + void iris$begin(MatrixStack matrixStack, BlockRenderPass pass); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderColumn.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderColumn.java new file mode 100644 index 000000000..57f989ecb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderColumn.java @@ -0,0 +1,81 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import net.minecraftforge.common.util.ForgeDirection; + +public class ChunkRenderColumn { + @SuppressWarnings("unchecked") + private final ChunkRenderContainer[] renders = new ChunkRenderContainer[16]; + + @SuppressWarnings("unchecked") + private final ChunkRenderColumn[] adjacent = new ChunkRenderColumn[6]; + + private final int x, z; + + public ChunkRenderColumn(int x, int z) { + this.x = x; + this.z = z; + + this.setAdjacentColumn(ForgeDirection.UP, this); + this.setAdjacentColumn(ForgeDirection.DOWN, this); + } + + public void setAdjacentColumn(ForgeDirection dir, ChunkRenderColumn column) { + this.adjacent[dir.ordinal()] = column; + } + + public ChunkRenderColumn getAdjacentColumn(ForgeDirection dir) { + return this.adjacent[dir.ordinal()]; + } + + public void setRender(int y, ChunkRenderContainer render) { + this.renders[y] = render; + } + + public ChunkRenderContainer getRender(int y) { + if (y < 0 || y >= this.renders.length) { + return null; + } + return this.renders[y]; + } + + public int getX() { + return this.x; + } + + public int getZ() { + return this.z; + } + + public boolean areNeighborsPresent() { + for (ForgeDirection dir : DirectionUtil.HORIZONTAL_DIRECTIONS) { + ChunkRenderColumn adj = this.adjacent[dir.ordinal()]; + + if (adj == null) { + return false; + } + + ForgeDirection corner; + + // Access the adjacent corner chunk from the neighbor in this direction + if (dir == ForgeDirection.NORTH) { + corner = ForgeDirection.EAST; + } else if (dir == ForgeDirection.SOUTH) { + corner = ForgeDirection.WEST; + } else if (dir == ForgeDirection.WEST) { + corner = ForgeDirection.NORTH; + } else if (dir == ForgeDirection.EAST) { + corner = ForgeDirection.SOUTH; + } else { + continue; + } + + // If no neighbor has been attached, the chunk is not present + if (adj.getAdjacentColumn(corner) == null) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderContainer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderContainer.java new file mode 100644 index 000000000..3c9e1684e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderContainer.java @@ -0,0 +1,306 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderBounds; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.texture.SpriteUtil; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +import java.lang.reflect.Array; +import java.util.concurrent.CompletableFuture; + +/** + * The render state object for a chunk section. This contains all the graphics state for each render pass along with + * data about the render in the chunk visibility graph. + */ +public class ChunkRenderContainer { + private final SodiumWorldRenderer worldRenderer; + @Getter + private final int chunkX; + @Getter + private final int chunkY; + @Getter + private final int chunkZ; + + @Getter + private final T[] graphicsStates; + private final ChunkRenderColumn column; + + @Getter + private ChunkRenderData data = ChunkRenderData.ABSENT; + private CompletableFuture rebuildTask = null; + + private boolean needsRebuild; + private boolean needsImportantRebuild; + + private boolean needsSort; + + @Getter + private boolean tickable; + @Getter + private int id; + + private boolean rebuildableForTranslucents; + + public ChunkRenderContainer(ChunkRenderBackend backend, SodiumWorldRenderer worldRenderer, int chunkX, int chunkY, int chunkZ, ChunkRenderColumn column) { + this.worldRenderer = worldRenderer; + + this.chunkX = chunkX; + this.chunkY = chunkY; + this.chunkZ = chunkZ; + + //noinspection unchecked + this.graphicsStates = (T[]) Array.newInstance(backend.getGraphicsStateType(), BlockRenderPass.COUNT); + this.rebuildableForTranslucents = false; + this.column = column; + } + + /** + * Cancels any pending tasks to rebuild the chunk. If the result of any pending tasks has not been processed yet, + * those will also be discarded when processing finally happens. + */ + public void cancelRebuildTask() { + this.needsRebuild = false; + this.needsImportantRebuild = false; + this.needsSort = false; + + if (this.rebuildTask != null) { + this.rebuildTask.cancel(false); + this.rebuildTask = null; + } + } + + /** + * @return True if the render's state is out of date with the world state + */ + public boolean needsRebuild() { + return this.needsRebuild; + } + + /** + * @return True if the render's rebuild should be performed as blocking + */ + public boolean needsImportantRebuild() { + return this.needsImportantRebuild; + } + + public boolean needsSort() { + return this.needsSort; + } + + /** + * Deletes all data attached to this render and drops any pending tasks. This should be used when the render falls + * out of view or otherwise needs to be destroyed. After the render has been destroyed, the object can no longer + * be used. + */ + public void delete() { + this.cancelRebuildTask(); + this.setData(ChunkRenderData.ABSENT); + this.deleteGraphicsState(); + } + + private void deleteGraphicsState() { + T[] states = this.graphicsStates; + + for (int i = 0; i < states.length; i++) { + T state = states[i]; + + if (state != null) { + state.delete(RenderDevice.INSTANCE.createCommandList()); + states[i] = null; + } + } + } + + public boolean shouldRebuildForTranslucents() { + return this.rebuildableForTranslucents; + } + + public void setRebuildForTranslucents(boolean flag) { + this.rebuildableForTranslucents = flag; + } + + + public void setData(ChunkRenderData info) { + if (info == null) { + throw new NullPointerException("Mesh information must not be null"); + } + + this.worldRenderer.onChunkRenderUpdated(this.chunkX, this.chunkY, this.chunkZ, this.data, info); + this.data = info; + + this.tickable = !info.getAnimatedSprites().isEmpty(); + } + + /** + * Marks this render as needing an update. Important updates are scheduled as "blocking" and will prevent the next + * frame from being rendered until the update is performed. + * @param important True if the update is blocking, otherwise false + */ + public boolean scheduleRebuild(boolean important) { + boolean changed = !this.needsRebuild || (!this.needsImportantRebuild && important); + + this.needsImportantRebuild = important; + this.needsRebuild = true; + this.needsSort = false; + + return changed; + } + + public boolean scheduleSort(boolean important) { + if (this.needsRebuild) + return false; + + boolean changed = !this.needsSort; + this.needsSort = true; + + return changed; + } + + /** + * @return True if the chunk render contains no data, otherwise false + */ + public boolean isEmpty() { + return this.data.isEmpty(); + } + + /** + * Returns the chunk section position which this render refers to in the world. + */ + public ChunkSectionPos getChunkPos() { + return ChunkSectionPos.from(this.chunkX, this.chunkY, this.chunkZ); + } + + /** + * Tests if the given chunk render is visible within the provided frustum. + * @param frustum The frustum to test against + * @return True if visible, otherwise false + */ + public boolean isOutsideFrustum(FrustumExtended frustum) { + float x = this.getOriginX(); + float y = this.getOriginY(); + float z = this.getOriginZ(); + + return !frustum.fastAabbTest(x, y, z, x + 16.0f, y + 16.0f, z + 16.0f); + } + + /** + * Ensures that all resources attached to the given chunk render are "ticked" forward. This should be called every + * time before this render is drawn if {@link ChunkRenderContainer#isTickable()} is true. + */ + public void tick() { + for (TextureAtlasSprite sprite : this.data.getAnimatedSprites()) { + SpriteUtil.markSpriteActive(sprite); + } + } + + /** + * @return The x-coordinate of the origin position of this chunk render + */ + public int getOriginX() { + return this.chunkX << 4; + } + + /** + * @return The y-coordinate of the origin position of this chunk render + */ + public int getOriginY() { + return this.chunkY << 4; + } + + /** + * @return The z-coordinate of the origin position of this chunk render + */ + public int getOriginZ() { + return this.chunkZ << 4; + } + + public int getRenderX() { + return this.getOriginX() - 8; + } + + public int getRenderY() { + return this.getOriginY() - 8; + } + + public int getRenderZ() { + return this.getOriginZ() - 8; + } + + /** + * @return The squared distance from the center of this chunk in the world to the given position + */ + public double getSquaredDistance(double x, double y, double z) { + double xDist = x - this.getCenterX(); + double yDist = y - this.getCenterY(); + double zDist = z - this.getCenterZ(); + + return (xDist * xDist) + (yDist * yDist) + (zDist * zDist); + } + + /** + * @return The x-coordinate of the center position of this chunk render + */ + private double getCenterX() { + return this.getOriginX() + 8.0D; + } + + /** + * @return The y-coordinate of the center position of this chunk render + */ + private double getCenterY() { + return this.getOriginY() + 8.0D; + } + + /** + * @return The z-coordinate of the center position of this chunk render + */ + private double getCenterZ() { + return this.getOriginZ() + 8.0D; + } + + public BlockPos getRenderOrigin() { + return new BlockPos(this.getRenderX(), this.getRenderY(), this.getRenderZ()); + } + + public void setGraphicsState(BlockRenderPass pass, T state) { + this.graphicsStates[pass.ordinal()] = state; + } + + /** + * @return The squared distance from the center of this chunk in the world to the given position + */ + public double getSquaredDistanceXZ(double x, double z) { + double xDist = x - this.getCenterX(); + double zDist = z - this.getCenterZ(); + + return (xDist * xDist) + (zDist * zDist); + } + + public ChunkRenderBounds getBounds() { + return this.data.getBounds(); + } + + public T getGraphicsState(BlockRenderPass pass) { + return this.graphicsStates[pass.ordinal()]; + } + + public int getFacesWithData() { + return this.data.getFacesWithData(); + } + + public boolean canRebuild() { + return this.column.areNeighborsPresent(); + } + + public void setId(int id) { + this.id = id; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java new file mode 100644 index 000000000..ddbc42c13 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java @@ -0,0 +1,702 @@ +package me.jellysquid.mods.sodium.client.render.chunk; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.mojang.ChunkPos; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongCollection; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.compat.FogHelper; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw.MultidrawChunkRenderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuilder; +import me.jellysquid.mods.sodium.client.render.chunk.cull.ChunkCuller; +import me.jellysquid.mods.sodium.client.render.chunk.cull.ChunkFaceFlags; +import me.jellysquid.mods.sodium.client.render.chunk.cull.graph.ChunkGraphCuller; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderBounds; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.lists.ChunkRenderList; +import me.jellysquid.mods.sodium.client.render.chunk.lists.ChunkRenderListIterator; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import me.jellysquid.mods.sodium.client.world.ChunkStatusListener; +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import me.jellysquid.mods.sodium.common.util.IdTable; +import me.jellysquid.mods.sodium.common.util.collections.FutureDequeDrain; +import net.coderbot.iris.shadows.ShadowRenderingState; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3d; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; +import java.util.concurrent.CompletableFuture; + +public class ChunkRenderManager implements ChunkStatusListener { + /** + * The maximum distance a chunk can be from the player's camera in order to be eligible for blocking updates. + */ + private static final double NEARBY_CHUNK_DISTANCE = Math.pow(48, 2.0); + + /** + * The minimum distance the culling plane can be from the player's camera. This helps to prevent mathematical + * errors that occur when the fog distance is less than 8 blocks in width, such as when using a blindness potion. + */ + private static final float FOG_PLANE_MIN_DISTANCE = (float) Math.pow(8.0f, 2.0); + + /** + * The distance past the fog's far plane at which to begin culling. Distance calculations use the center of each + * chunk from the camera's position, and as such, special care is needed to ensure that the culling plane is pushed + * back far enough. I'm sure there's a mathematical formula that should be used here in place of the constant, + * but this value works fine in testing. + */ + private static final float FOG_PLANE_OFFSET = 12.0f; + + private final ChunkBuilder builder; + private final ChunkRenderBackend backend; + + private final Thread renderThread = Thread.currentThread(); + + private final Long2ObjectOpenHashMap> columns = new Long2ObjectOpenHashMap<>(); + private final IdTable> renders = new IdTable<>(16384); + + private final ObjectArrayFIFOQueue> importantRebuildQueue = new ObjectArrayFIFOQueue<>(); + private final ObjectArrayFIFOQueue> rebuildQueue = new ObjectArrayFIFOQueue<>(); + private final ObjectArrayFIFOQueue> sortQueue = new ObjectArrayFIFOQueue<>(); + private final ObjectArrayFIFOQueue> unloadQueue = new ObjectArrayFIFOQueue<>(); + + @SuppressWarnings("unchecked") + private ChunkRenderList[] chunkRenderLists = new ChunkRenderList[BlockRenderPass.COUNT]; + private ObjectList> tickableChunks = new ObjectArrayList<>(); + + private ObjectList visibleTileEntities = new ObjectArrayList<>(); + + private final SodiumWorldRenderer renderer; + private final WorldClient world; + + private final ChunkCuller culler; + private final boolean useBlockFaceCulling; + + private float cameraX, cameraY, cameraZ; + private boolean dirty; + + @Getter + private int submitted; + private int totalSubmitted; + + private final boolean translucencySorting; + + @Getter + private int visibleChunkCount; + + private boolean useFogCulling; + private double fogRenderCutoff; + + private final int translucencyBlockRenderDistance; + + private boolean alwaysDeferChunkUpdates; + + // Iris + + private ChunkRenderList[] chunkRenderListsSwap; + + private ObjectList> tickableChunksSwap; + + private ObjectList visibleTileEntitiesSwap; + + private int visibleChunkCountSwap; + + private boolean dirtySwap; + + private static final ObjectArrayFIFOQueue EMPTY_QUEUE = new ObjectArrayFIFOQueue<>(); + + + + public ChunkRenderManager(SodiumWorldRenderer renderer, ChunkRenderBackend backend, WorldClient world, int renderDistance) { + this.backend = backend; + this.renderer = renderer; + this.world = world; + + this.builder = new ChunkBuilder<>(backend.getVertexType(), this.backend); + this.builder.init(world); + + this.dirty = true; + + for (int i = 0; i < this.chunkRenderLists.length; i++) { + this.chunkRenderLists[i] = new ChunkRenderList<>(); + } + + this.culler = new ChunkGraphCuller(world, renderDistance); + this.translucencySorting = SodiumClientMod.options().advanced.translucencySorting; + this.translucencyBlockRenderDistance = Math.min(9216, (renderDistance << 4) * (renderDistance << 4)); + + this.useBlockFaceCulling = SodiumClientMod.options().advanced.useBlockFaceCulling; + + if(AngelicaConfig.enableIris) { + this.chunkRenderListsSwap = new ChunkRenderList[BlockRenderPass.COUNT]; + this.tickableChunksSwap = new ObjectArrayList<>(); + this.visibleTileEntitiesSwap = new ObjectArrayList<>(); + + for (int i = 0; i < this.chunkRenderListsSwap.length; i++) { + this.chunkRenderListsSwap[i] = new ChunkRenderList<>(); + } + + this.dirtySwap = true; + } + } + + public void update(Camera camera, FrustumExtended frustum, int frame, boolean spectator) { + this.reset(); + this.unloadPending(); + + this.setup(camera); + this.iterateChunks(camera, frustum, frame, spectator); + + this.dirty = false; + } + + public int getRebuildQueueSize() { + return this.rebuildQueue.size(); + } + + public int getImportantRebuildQueueSize() { + return this.importantRebuildQueue.size(); + } + + private void setup(Camera camera) { + Vector3d cameraPos = camera.getPos(); + + this.cameraX = (float) cameraPos.x; + this.cameraY = (float) cameraPos.y; + this.cameraZ = (float) cameraPos.z; + + this.useFogCulling = false; + this.alwaysDeferChunkUpdates = SodiumClientMod.options().performance.alwaysDeferChunkUpdates; + + if (SodiumClientMod.options().advanced.useFogOcclusion) { + float dist = FogHelper.getFogCutoff() + FOG_PLANE_OFFSET; + + if (dist != 0.0f) { + this.useFogCulling = true; + this.fogRenderCutoff = Math.max(FOG_PLANE_MIN_DISTANCE, dist * dist); + } + } + } + + private void iterateChunks(Camera camera, FrustumExtended frustum, int frame, boolean spectator) { + // Schedule new translucency sorting tasks if the camera has moved + if(this.translucencySorting) { + this.checkTranslucencyCameraMoved(); + if(this.hasCameraMovedTranslucent) { + for(Object o : this.renders.getElements()) { + if(o == null) + continue; + ChunkRenderContainer render = (ChunkRenderContainer)o; + if(render.getData().isEmpty()) + continue; + if(!render.needsRebuild() && render.canRebuild() && render.shouldRebuildForTranslucents() && render.getSquaredDistance(cameraX, cameraY, cameraZ) < translucencyBlockRenderDistance) { + // put it at the end of the queue, after any "real" rebuild tasks + render.scheduleSort(false); + } + } + } + } + + IntList list = this.culler.computeVisible(camera, frustum, frame, spectator); + IntIterator it = list.iterator(); + + while (it.hasNext()) { + ChunkRenderContainer render = this.renders.get(it.nextInt()); + + this.addChunk(render); + } + } + + private float lastCameraTranslucentX, lastCameraTranslucentY, lastCameraTranslucentZ; + private boolean hasCameraMovedTranslucent; + + private void checkTranslucencyCameraMoved() { + float dx = (cameraX - lastCameraTranslucentX); + float dy = (cameraY - lastCameraTranslucentY); + float dz = (cameraZ - lastCameraTranslucentZ); + if((dx * dx + dy * dy + dz * dz) > 1.0) { + lastCameraTranslucentX = cameraX; + lastCameraTranslucentY = cameraY; + lastCameraTranslucentZ = cameraZ; + hasCameraMovedTranslucent = true; + } else + hasCameraMovedTranslucent = false; + } + + private void addChunk(ChunkRenderContainer render) { + final boolean canRebuild = AngelicaConfig.enableIris ? !ShadowRenderingState.areShadowsCurrentlyBeingRendered() : render.canRebuild(); + + if (render.needsRebuild() && canRebuild) { + if (!this.alwaysDeferChunkUpdates && render.needsImportantRebuild()) { + this.importantRebuildQueue.enqueue(render); + } else { + this.rebuildQueue.enqueue(render); + } + } else if (canRebuild && !render.getData().isEmpty() && render.needsSort()) { + this.sortQueue.enqueue(render); + } + + if (this.useFogCulling && render.getSquaredDistanceXZ(this.cameraX, this.cameraZ) >= this.fogRenderCutoff) { + return; + } + + if (!render.isEmpty()) { + this.addChunkToRenderLists(render); + this.addEntitiesToRenderLists(render); + } + } + + private void addChunkToRenderLists(ChunkRenderContainer render) { + int visibleFaces = this.computeVisibleFaces(render) & render.getFacesWithData(); + + if (visibleFaces == 0) { + return; + } + + boolean added = false; + T[] states = render.getGraphicsStates(); + + for (int i = 0; i < states.length; i++) { + T state = states[i]; + + if (state != null) { + ChunkRenderList list = this.chunkRenderLists[i]; + list.add(state, (this.translucencySorting && BlockRenderPass.VALUES[i].isTranslucent()) ? (ChunkFaceFlags.ALL & render.getFacesWithData()) : visibleFaces); + + added = true; + } + } + + if (added) { + if (render.isTickable()) { + this.tickableChunks.add(render); + } + + this.visibleChunkCount++; + } + } + + private int computeVisibleFaces(ChunkRenderContainer render) { + if(AngelicaConfig.enableIris) { + // TODO: Enable chunk face culling during the shadow pass + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + return(ChunkFaceFlags.ALL); + } + } + // If chunk face culling is disabled, render all faces + if (!this.useBlockFaceCulling) { + return ChunkFaceFlags.ALL; + } + + ChunkRenderBounds bounds = render.getBounds(); + + // Always render groups of vertices not belonging to any given face + int visibleFaces = ChunkFaceFlags.UNASSIGNED; + + if (this.cameraY > bounds.y1) { + visibleFaces |= ChunkFaceFlags.UP; + } + + if (this.cameraY < bounds.y2) { + visibleFaces |= ChunkFaceFlags.DOWN; + } + + if (this.cameraX > bounds.x1) { + visibleFaces |= ChunkFaceFlags.EAST; + } + + if (this.cameraX < bounds.x2) { + visibleFaces |= ChunkFaceFlags.WEST; + } + + if (this.cameraZ > bounds.z1) { + visibleFaces |= ChunkFaceFlags.SOUTH; + } + + if (this.cameraZ < bounds.z2) { + visibleFaces |= ChunkFaceFlags.NORTH; + } + + return visibleFaces; + } + + private void addEntitiesToRenderLists(ChunkRenderContainer render) { + Collection tileEntities = render.getData().getTileEntities(); + + if (!tileEntities.isEmpty()) { + this.visibleTileEntities.addAll(tileEntities); + } + } + + public ChunkRenderContainer getRender(int x, int y, int z) { + ChunkRenderColumn column = this.columns.get(ChunkPos.toLong(x, z)); + + if (column == null) { + return null; + } + + return column.getRender(y); + } + + private void reset() { + if(!AngelicaConfig.enableIris || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.rebuildQueue.clear(); + if(!AngelicaConfig.enableIris || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.importantRebuildQueue.clear(); + + + this.sortQueue.clear(); + + this.visibleTileEntities.clear(); + + for (ChunkRenderList list : this.chunkRenderLists) { + list.reset(); + } + + this.tickableChunks.clear(); + + this.visibleChunkCount = 0; + } + + private void unloadPending() { + while (!this.unloadQueue.isEmpty()) { + this.unloadQueue.dequeue() + .delete(); + } + } + + public Collection getVisibleTileEntities() { + return this.visibleTileEntities; + } + + @Override + public void onChunkAdded(int x, int z) { + this.loadChunk(x, z); + } + + @Override + public void onChunkRemoved(int x, int z) { + this.unloadChunk(x, z); + } + + private void loadChunk(int x, int z) { + ChunkRenderColumn column = new ChunkRenderColumn<>(x, z); + ChunkRenderColumn prev; + + if ((prev = this.columns.put(ChunkPos.toLong(x, z), column)) != null) { + this.unloadSections(prev); + } + + this.connectNeighborColumns(column); + this.loadSections(column); + + this.dirty = true; + } + + private void unloadChunk(int x, int z) { + ChunkRenderColumn column = this.columns.remove(ChunkPos.toLong(x, z)); + + if (column == null) { + return; + } + + this.disconnectNeighborColumns(column); + this.unloadSections(column); + + this.dirty = true; + } + + private void loadSections(ChunkRenderColumn column) { + int x = column.getX(); + int z = column.getZ(); + + for (int y = 0; y < 16; y++) { + ChunkRenderContainer render = this.createChunkRender(column, x, y, z); + column.setRender(y, render); + + this.culler.onSectionLoaded(x, y, z, render.getId()); + } + } + + private void unloadSections(ChunkRenderColumn column) { + int x = column.getX(); + int z = column.getZ(); + + for (int y = 0; y < 16; y++) { + ChunkRenderContainer render = column.getRender(y); + + if (render != null) { + this.unloadQueue.enqueue(render); + this.renders.remove(render.getId()); + } + + this.culler.onSectionUnloaded(x, y, z); + } + } + + private void connectNeighborColumns(ChunkRenderColumn column) { + for (ForgeDirection dir : DirectionUtil.ALL_DIRECTIONS) { + ChunkRenderColumn adj = this.getAdjacentColumn(column, dir); + + if (adj != null) { + adj.setAdjacentColumn(dir.getOpposite(), column); + } + + column.setAdjacentColumn(dir, adj); + } + } + + private void disconnectNeighborColumns(ChunkRenderColumn column) { + for (ForgeDirection dir : DirectionUtil.ALL_DIRECTIONS) { + ChunkRenderColumn adj = column.getAdjacentColumn(dir); + + if (adj != null) { + adj.setAdjacentColumn(dir.getOpposite(), null); + } + + column.setAdjacentColumn(dir, null); + } + } + + private ChunkRenderColumn getAdjacentColumn(ChunkRenderColumn column, ForgeDirection dir) { + return this.getColumn(column.getX() + dir.offsetX, column.getZ() + dir.offsetZ); + } + + private ChunkRenderColumn getColumn(int x, int z) { + return this.columns.get(ChunkPos.toLong(x, z)); + } + + private ChunkRenderContainer createChunkRender(ChunkRenderColumn column, int x, int y, int z) { + ChunkRenderContainer render = new ChunkRenderContainer<>(this.backend, this.renderer, x, y, z, column); + final ExtendedBlockStorage array = this.world.getChunkFromChunkCoords(x, z).getBlockStorageArray()[y]; + + if (array == null || array.isEmpty()) { + render.setData(ChunkRenderData.EMPTY); + } else { + render.scheduleRebuild(false); + } + + render.setId(this.renders.add(render)); + + return render; + } + + public void renderLayer(MatrixStack matrixStack, BlockRenderPass pass, double x, double y, double z) { + final ChunkRenderList chunkRenderList = this.chunkRenderLists[pass.ordinal()]; + final ChunkRenderListIterator iterator = chunkRenderList.iterator(pass.isTranslucent()); + + final RenderDevice device = RenderDevice.INSTANCE; + final CommandList commandList = device.createCommandList(); + if(AngelicaConfig.enableIris) this.backend.iris$begin(matrixStack, pass); + else this.backend.begin(matrixStack); + + // Ensure multidraw regions are ordered appropriately + if(this.backend instanceof MultidrawChunkRenderBackend) { + ((MultidrawChunkRenderBackend) this.backend).setReverseRegions(pass.isTranslucent()); + } + this.backend.render(commandList, iterator, new ChunkCameraContext(x, y, z)); + this.backend.end(matrixStack); + + commandList.flush(); + } + + public void tickVisibleRenders() { + for (ChunkRenderContainer render : this.tickableChunks) { + render.tick(); + } + } + + public boolean isChunkVisible(int x, int y, int z) { + return this.culler.isSectionVisible(x, y, z); + } + + public void updateChunks() { + if (AngelicaConfig.enableIris && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) return; + this.builder.cleanupSectionCache(); + + Deque>> futures = new ArrayDeque<>(); + + int budget = this.builder.getSchedulingBudget(); + submitted = 0; + + while (!this.importantRebuildQueue.isEmpty()) { + ChunkRenderContainer render = this.importantRebuildQueue.dequeue(); + + if (render == null) { + continue; + } + + // Do not allow distant chunks to block rendering + if (this.alwaysDeferChunkUpdates || !this.isChunkPrioritized(render)) { + this.builder.deferRebuild(render); + } else { + futures.add(this.builder.scheduleRebuildTaskAsync(render)); + } + + this.dirty = true; + submitted++; + // Limit quantity of updates submitted if we are deferring all important builds + if (this.alwaysDeferChunkUpdates && submitted >= budget) + break; + } + + while (submitted < budget && !this.rebuildQueue.isEmpty()) { + ChunkRenderContainer render = this.rebuildQueue.dequeue(); + + this.builder.deferRebuild(render); + submitted++; + } + + // always do at least one sort + boolean sortedAnything = false; + while ((!sortedAnything || submitted < budget) && !this.sortQueue.isEmpty()) { + ChunkRenderContainer render = this.sortQueue.dequeue(); + + this.builder.deferSort(render); + sortedAnything = true; + submitted++; + } + totalSubmitted += submitted; + + this.dirty |= submitted > 0; + + // Try to complete some other work on the main thread while we wait for rebuilds to complete + this.dirty |= this.builder.performPendingUploads(); + + if (!futures.isEmpty()) { + this.dirty = true; + this.backend.upload(RenderDevice.INSTANCE.createCommandList(), new FutureDequeDrain<>(futures)); + } + } + + public int getAndResetSubmitted() { + // Return how many chunks were submitted since the last call to this method + final int submitted = totalSubmitted; + totalSubmitted = 0; + return submitted; + } + + public void markDirty() { + this.dirty = true; + } + + public boolean isDirty() { + return this.dirty; + } + + public void restoreChunks(LongCollection chunks) { + LongIterator it = chunks.iterator(); + + while (it.hasNext()) { + long pos = it.nextLong(); + + this.loadChunk(ChunkPos.getPackedX(pos), ChunkPos.getPackedZ(pos)); + } + } + + public boolean isBuildComplete() { + return this.builder.isBuildQueueEmpty(); + } + + public void setCameraPosition(double x, double y, double z) { + this.builder.setCameraPosition(x, y, z); + } + + public void destroy() { + this.reset(); + + for (ChunkRenderColumn column : this.columns.values()) { + this.unloadSections(column); + } + + this.columns.clear(); + + this.builder.stopWorkers(); + } + + public int getTotalSections() { + return this.columns.size() * 16; + } + + private void scheduleRebuildOffThread(int x, int y, int z, boolean important) { + // TODO: Sodium Threads + throw new RuntimeException("scheduleRebuildOffThread"); +// Minecraft.getMinecraft().submit(() -> this.scheduleRebuild(x, y, z, important)); + } + + public void scheduleRebuild(int x, int y, int z, boolean important) { + // Some mods try to schedule rebuilds off the main thread. This is dangerous, + // but the vanilla renderer seems to tolerate it. Our hashmaps are not thread-safe + // so the best solution is to do what the server chunk system does and handle this + // on the main thread. + if (Thread.currentThread() != renderThread) { + this.scheduleRebuildOffThread(x, y, z, important); + return; + } + + ChunkRenderContainer render = this.getRender(x, y, z); + + if (render != null) { + // Nearby chunks are always rendered immediately + important = important || this.isChunkPrioritized(render); + + // Only enqueue chunks for updates if they aren't already enqueued for an update + if (render.scheduleRebuild(important) && render.canRebuild()) { + (render.needsImportantRebuild() ? this.importantRebuildQueue : this.rebuildQueue) + .enqueue(render); + } + + this.dirty = true; + } + + this.builder.onChunkDataChanged(x, y, z); + } + + public boolean isChunkPrioritized(ChunkRenderContainer render) { + return render != null ? render.getSquaredDistance(this.cameraX, this.cameraY, this.cameraZ) <= NEARBY_CHUNK_DISTANCE : false; + } + + public void onChunkRenderUpdates(int x, int y, int z, ChunkRenderData data) { + this.culler.onSectionStateChanged(x, y, z, data.getOcclusionData()); + } + + // Iris + public void iris$swapVisibilityState() { + ChunkRenderList[] chunkRenderListsTmp = chunkRenderLists; + chunkRenderLists = chunkRenderListsSwap; + chunkRenderListsSwap = chunkRenderListsTmp; + + ObjectList> tickableChunksTmp = tickableChunks; + tickableChunks = tickableChunksSwap; + tickableChunksSwap = tickableChunksTmp; + + ObjectList visibleTileEntitiesTmp = visibleTileEntities; + visibleTileEntities = visibleTileEntitiesSwap; + visibleTileEntitiesSwap = visibleTileEntitiesTmp; + + int visibleChunkCountTmp = visibleChunkCount; + visibleChunkCount = visibleChunkCountSwap; + visibleChunkCountSwap = visibleChunkCountTmp; + + boolean dirtyTmp = dirty; + dirty = dirtySwap; + dirtySwap = dirtyTmp; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawCallBatcher.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawCallBatcher.java new file mode 100644 index 000000000..aeb88fbcf --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawCallBatcher.java @@ -0,0 +1,126 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import com.gtnewhorizons.angelica.compat.mojang.CompatMathHelper; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import org.lwjgl.MemoryUtil; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Provides a fixed-size buffer which can be used to batch chunk section draw calls. + */ +public abstract class ChunkDrawCallBatcher extends StructBuffer { + protected final int capacity; + + protected boolean isBuilding; + protected int count; + + protected int arrayLength; + + protected ChunkDrawCallBatcher(int capacity) { + super(CompatMathHelper.smallestEncompassingPowerOfTwo(capacity), 16); + + this.capacity = capacity; + } + + public static ChunkDrawCallBatcher create(int capacity) { + return SodiumClientMod.isDirectMemoryAccessEnabled() ? new UnsafeChunkDrawCallBatcher(capacity) : new NioChunkDrawCallBatcher(capacity); + } + + public void begin() { + this.isBuilding = true; + this.count = 0; + this.arrayLength = 0; + + this.buffer.limit(this.buffer.capacity()); + } + + public void end() { + this.isBuilding = false; + + this.arrayLength = this.count * this.stride; + this.buffer.limit(this.arrayLength); + this.buffer.position(0); + } + + public boolean isBuilding() { + return this.isBuilding; + } + + public abstract void addIndirectDrawCall(int first, int count, int baseInstance, int instanceCount); + + public int getCount() { + return this.count; + } + + public boolean isEmpty() { + return this.count <= 0; + } + + public static class UnsafeChunkDrawCallBatcher extends ChunkDrawCallBatcher { + + private final long basePointer; + private long writePointer; + + public UnsafeChunkDrawCallBatcher(int capacity) { + super(capacity); + + this.basePointer = MemoryUtil.getAddress(this.buffer); + } + + @Override + public void begin() { + super.begin(); + + this.writePointer = this.basePointer; + } + + @Override + public void addIndirectDrawCall(int first, int count, int baseInstance, int instanceCount) { + if (this.count++ >= this.capacity) { + throw new BufferUnderflowException(); + } + + CompatMemoryUtil.memPutInt(this.writePointer , count); // Vertex Count + CompatMemoryUtil.memPutInt(this.writePointer + 4, instanceCount); // Instance Count + CompatMemoryUtil.memPutInt(this.writePointer + 8, first); // Vertex Start + CompatMemoryUtil.memPutInt(this.writePointer + 12, baseInstance); // Base Instance + + this.writePointer += this.stride; + } + } + + public static class NioChunkDrawCallBatcher extends ChunkDrawCallBatcher { + private int writeOffset; + + public NioChunkDrawCallBatcher(int capacity) { + super(capacity); + } + + @Override + public void begin() { + super.begin(); + + this.writeOffset = 0; + } + + @Override + public void addIndirectDrawCall(int first, int count, int baseInstance, int instanceCount) { + ByteBuffer buf = this.buffer; + buf.putInt(this.writeOffset , count); // Vertex Count + buf.putInt(this.writeOffset + 4, instanceCount); // Instance Count + buf.putInt(this.writeOffset + 8, first); // Vertex Start + buf.putInt(this.writeOffset + 12, baseInstance); // Base Instance + + this.writeOffset += this.stride; + this.count++; + } + } + + public int getArrayLength() { + return this.arrayLength; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawParamsVector.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawParamsVector.java new file mode 100644 index 000000000..05b71837e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/ChunkDrawParamsVector.java @@ -0,0 +1,107 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import org.lwjgl.MemoryUtil; + +import java.nio.ByteBuffer; + +/** + * Provides a resizeable vector backed by native memory that can be used to build an array of chunk draw call + * parameters. + */ +public abstract class ChunkDrawParamsVector extends StructBuffer { + protected int capacity; + protected int count; + + protected ChunkDrawParamsVector(int capacity) { + super(capacity, 16); + + this.capacity = capacity; + } + + public static ChunkDrawParamsVector create(int capacity) { + return SodiumClientMod.isDirectMemoryAccessEnabled() ? new UnsafeChunkDrawCallVector(capacity) : new NioChunkDrawCallVector(capacity); + } + + public abstract void pushChunkDrawParams(float x, float y, float z); + + public void reset() { + this.count = 0; + } + + protected void growBuffer() { + this.capacity = this.capacity * 2; + this.buffer = CompatMemoryUtil.memReallocDirect(this.buffer, this.capacity * this.stride); + } + + public static class UnsafeChunkDrawCallVector extends ChunkDrawParamsVector { + private long basePointer; + private long writePointer; + + public UnsafeChunkDrawCallVector(int capacity) { + super(capacity); + + this.basePointer = MemoryUtil.getAddress(this.buffer); + } + + @Override + public void pushChunkDrawParams(float x, float y, float z) { + if (this.count++ >= this.capacity) { + this.growBuffer(); + } + + CompatMemoryUtil.memPutFloat(this.writePointer , x); + CompatMemoryUtil.memPutFloat(this.writePointer + 4, y); + CompatMemoryUtil.memPutFloat(this.writePointer + 8, z); + + this.writePointer += this.stride; + } + + @Override + protected void growBuffer() { + super.growBuffer(); + + long offset = this.writePointer - this.basePointer; + + this.basePointer = MemoryUtil.getAddress(this.buffer); + this.writePointer = this.basePointer + offset; + } + + @Override + public void reset() { + super.reset(); + + this.writePointer = this.basePointer; + } + } + + public static class NioChunkDrawCallVector extends ChunkDrawParamsVector { + private int writeOffset; + + public NioChunkDrawCallVector(int capacity) { + super(capacity); + } + + @Override + public void pushChunkDrawParams(float x, float y, float z) { + if (this.count++ >= this.capacity) { + this.growBuffer(); + } + + ByteBuffer buf = this.buffer; + buf.putFloat(this.writeOffset , x); + buf.putFloat(this.writeOffset + 4, y); + buf.putFloat(this.writeOffset + 8, z); + + this.writeOffset += this.stride; + } + + @Override + public void reset() { + super.reset(); + + this.writeOffset = 0; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/IndirectCommandBufferVector.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/IndirectCommandBufferVector.java new file mode 100644 index 000000000..37cf45484 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/IndirectCommandBufferVector.java @@ -0,0 +1,37 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; + +public class IndirectCommandBufferVector extends StructBuffer { + protected IndirectCommandBufferVector(int capacity) { + super(capacity, 16); + } + + public static IndirectCommandBufferVector create(int capacity) { + return new IndirectCommandBufferVector(capacity); + } + + public void begin() { + this.buffer.clear(); + } + + public void end() { + if(this.buffer.position() > 0) { + this.buffer.flip(); + } + } + + public void pushCommandBuffer(ChunkDrawCallBatcher batcher) { + int len = batcher.getArrayLength(); + + if (this.buffer.remaining() < len) { + this.growBuffer(len); + } + + this.buffer.put(batcher.getBuffer()); + } + + protected void growBuffer(int n) { + this.buffer = CompatMemoryUtil.memReallocDirect(this.buffer, Math.max(this.buffer.capacity() * 2, this.buffer.capacity() + n)); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java new file mode 100644 index 000000000..0705b9695 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java @@ -0,0 +1,468 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import me.jellysquid.mods.sodium.client.gl.arena.GlBufferArena; +import me.jellysquid.mods.sodium.client.gl.arena.GlBufferSegment; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttribute; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTarget; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.DrawCommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.func.GlFunctions; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlPrimitiveType; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.TessellationBinding; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkCameraContext; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.lists.ChunkRenderListIterator; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.chunk.region.ChunkRegion; +import me.jellysquid.mods.sodium.client.render.chunk.region.ChunkRegionManager; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkRenderShaderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.sodium.IrisChunkShaderBindingPoints; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.Util; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Shader-based chunk renderer which makes use of a custom memory allocator on top of large buffer objects to allow + * for draw call batching without buffer switching. + * + * The biggest bottleneck after setting up vertex attribute state is the sheer number of buffer switches and draw calls + * being performed. In vanilla, the game uses one buffer for every chunk section, which means we need to bind, setup, + * and draw every chunk individually. + * + * In order to reduce the number of these calls, we need to firstly reduce the number of buffer switches. We do this + * through sub-dividing the world into larger "chunk regions" which then have one large buffer object in OpenGL. From + * here, we can allocate slices of this buffer to each individual chunk and then only bind it once before drawing. Then, + * our draw calls can simply point to individual sections within the buffer by manipulating the offset and count + * parameters. + * + * However, an unfortunate consequence is that if we run out of space in a buffer, we need to re-allocate the entire + * storage, which can take a ton of time! With old OpenGL 2.1 code, the only way to do this would be to copy the buffer's + * memory from the graphics card over the host bus into CPU memory, allocate a new buffer, and then copy it back over + * the bus and into graphics card. For reasons that should be obvious, this is extremely inefficient and requires the + * CPU and GPU to be synchronized. + * + * If we make use of more modern OpenGL 3.0 features, we can avoid this transfer over the memory bus and instead just + * perform the copy between buffers in GPU memory with the aptly named "copy buffer" function. It's still not blazing + * fast, but it's much better than what we're stuck with in older versions. We can help prevent these re-allocations by + * sizing our buffers to be a bit larger than what we expect all the chunk data to be, but this wastes memory. + * + * In the initial implementation, this solution worked fine enough, but the amount of time being spent on uploading + * chunks to the large buffers was now a magnitude more than what it was before all of this and it made chunk updates + * *very* slow. It took some tinkering to figure out what was going wrong here, but at least on the NVIDIA drivers, it + * seems that updating sub-regions of buffer memory hits some kind of slow path. A workaround for this problem is to + * create a scratch buffer object and upload the chunk data there *first*, re-allocating the storage each time. Then, + * you can copy the contents of the scratch buffer into the chunk region buffer, rise and repeat. I'm not happy with + * this solution, but it performs surprisingly well across all hardware I tried. + * + * With both of these changes, the amount of CPU time taken by rendering chunks linearly decreases with the reduction + * in buffer bind/setup/draw calls. Using the default settings of 4x2x4 chunk region buffers, the number of calls can be + * reduced up to a factor of ~32x. + */ +public class MultidrawChunkRenderBackend extends ChunkRenderShaderBackend { + private final ChunkRegionManager bufferManager; + + private final ObjectArrayList> pendingBatches = new ObjectArrayList<>(); + private final ObjectArrayFIFOQueue> pendingUploads = new ObjectArrayFIFOQueue<>(); + + private final GlMutableBuffer uploadBuffer; + private final GlMutableBuffer uniformBuffer; + private final GlMutableBuffer commandBuffer; + + private final ChunkDrawParamsVector uniformBufferBuilder; + private final IndirectCommandBufferVector commandClientBufferBuilder; + + public MultidrawChunkRenderBackend(RenderDevice device, ChunkVertexType vertexType) { + super(vertexType); + + this.bufferManager = new ChunkRegionManager<>(device); + + try (CommandList commands = device.createCommandList()) { + this.uploadBuffer = commands.createMutableBuffer(GlBufferUsage.GL_STREAM_DRAW); + this.uniformBuffer = commands.createMutableBuffer(GlBufferUsage.GL_STATIC_DRAW); + this.commandBuffer = isWindowsIntelDriver() ? null : commands.createMutableBuffer(GlBufferUsage.GL_STREAM_DRAW); + } + + this.uniformBufferBuilder = ChunkDrawParamsVector.create(2048); + this.commandClientBufferBuilder = IndirectCommandBufferVector.create(2048); + } + + @Override + public void upload(CommandList commandList, Iterator> queue) { + if(queue != null) { + this.setupUploadBatches(queue); + } + + commandList.bindBuffer(GlBufferTarget.ARRAY_BUFFER, this.uploadBuffer); + + while (!this.pendingUploads.isEmpty()) { + ChunkRegion region = this.pendingUploads.dequeue(); + + GlBufferArena arena = region.getBufferArena(); + GlBuffer buffer = arena.getBuffer(); + + ObjectArrayList> uploadQueue = region.getUploadQueue(); + arena.prepareBuffer(commandList, getUploadQueuePayloadSize(uploadQueue)); + + for (ChunkBuildResult result : uploadQueue) { + ChunkRenderContainer render = result.render; + ChunkRenderData data = result.data; + + for (BlockRenderPass pass : result.passesToUpload) { + MultidrawGraphicsState graphics = render.getGraphicsState(pass); + + // De-allocate the existing buffer arena for this render + // This will allow it to be cheaply re-allocated just below + if (graphics != null) { + graphics.delete(commandList); + } + + ChunkMeshData meshData = data.getMesh(pass); + + if (meshData.hasVertexData()) { + VertexData upload = meshData.takeVertexData(); + + commandList.uploadData(this.uploadBuffer, upload.buffer); + + GlBufferSegment segment = arena.uploadBuffer(commandList, this.uploadBuffer, 0, upload.buffer.capacity()); + + MultidrawGraphicsState graphicsState = new MultidrawGraphicsState(render, region, segment, meshData, this.vertexFormat); + if(pass.isTranslucent()) { + upload.buffer.limit(upload.buffer.capacity()); + upload.buffer.position(0); + + graphicsState.setTranslucencyData(upload.buffer); + } + render.setGraphicsState(pass, graphicsState); + } else { + render.setGraphicsState(pass, null); + } + } + + render.setData(data); + } + + // Check if the tessellation needs to be updated + // This happens whenever the backing buffer object for the arena changes, or if it hasn't already been created + if (region.getTessellation() == null || buffer != arena.getBuffer()) { + if (region.getTessellation() != null) { + commandList.deleteTessellation(region.getTessellation()); + } + + region.setTessellation(this.createRegionTessellation(commandList, arena.getBuffer())); + } + + uploadQueue.clear(); + } + + commandList.invalidateBuffer(this.uploadBuffer); + } + + private GlTessellation createRegionTessellation(CommandList commandList, GlBuffer buffer) { + return commandList.createTessellation(GlPrimitiveType.QUADS, new TessellationBinding[] { + new TessellationBinding(buffer,getBindings(), false), + new TessellationBinding(this.uniformBuffer, new GlVertexAttributeBinding[] { + new GlVertexAttributeBinding(ChunkShaderBindingPoints.MODEL_OFFSET, new GlVertexAttribute(GlVertexAttributeFormat.FLOAT, 4, false, 0, 0)) + }, true) + }); + } + + private GlVertexAttributeBinding[] getBindings() { + if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { + return new GlVertexAttributeBinding[] { + new GlVertexAttributeBinding(ChunkShaderBindingPoints.POSITION, this.vertexFormat.getAttribute(ChunkMeshAttribute.POSITION)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.COLOR, this.vertexFormat.getAttribute(ChunkMeshAttribute.COLOR)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.TEX_COORD, this.vertexFormat.getAttribute(ChunkMeshAttribute.TEXTURE)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.LIGHT_COORD, this.vertexFormat.getAttribute(ChunkMeshAttribute.LIGHT)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.NORMAL, vertexFormat.getAttribute(ChunkMeshAttribute.NORMAL)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.TANGENT, vertexFormat.getAttribute(ChunkMeshAttribute.TANGENT)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.MID_TEX_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.MID_TEX_COORD)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.BLOCK_ID, vertexFormat.getAttribute(ChunkMeshAttribute.BLOCK_ID)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.MID_BLOCK, vertexFormat.getAttribute(ChunkMeshAttribute.MID_BLOCK)) + }; + } else { + return new GlVertexAttributeBinding[] { + new GlVertexAttributeBinding(ChunkShaderBindingPoints.POSITION, this.vertexFormat.getAttribute(ChunkMeshAttribute.POSITION)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.COLOR, this.vertexFormat.getAttribute(ChunkMeshAttribute.COLOR)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.TEX_COORD, this.vertexFormat.getAttribute(ChunkMeshAttribute.TEXTURE)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.LIGHT_COORD, this.vertexFormat.getAttribute(ChunkMeshAttribute.LIGHT)) }; + } + + } + + private boolean reverseRegions = false; + private ChunkCameraContext regionCamera; + + /** + * Sets whether to reverse the order in which regions are drawn (fixes + */ + public void setReverseRegions(boolean flag) { + this.reverseRegions = flag; + } + + @Override + public void render(CommandList commandList, ChunkRenderListIterator renders, ChunkCameraContext camera) { + this.bufferManager.cleanup(); + + this.setupDrawBatches(commandList, renders, camera); + this.buildCommandBuffer(); + + if (this.commandBuffer != null) { + commandList.bindBuffer(GlBufferTarget.DRAW_INDIRECT_BUFFER, this.commandBuffer); + commandList.uploadData(this.commandBuffer, this.commandClientBufferBuilder.getBuffer()); + } + + long pointer = this.commandBuffer == null ? this.commandClientBufferBuilder.getBufferAddress() : 0L; + + for (ChunkRegion region : this.pendingBatches) { + ChunkDrawCallBatcher batch = region.getDrawBatcher(); + + if (!batch.isEmpty()) { + try (DrawCommandList drawCommandList = commandList.beginTessellating(region.getTessellation())) { + drawCommandList.multiDrawArraysIndirect(pointer, batch.getCount(), 0 /* tightly packed */); + } + } + + pointer += batch.getArrayLength(); + } + + this.pendingBatches.clear(); + } + + private static final Comparator> REGION_REVERSER = Comparator.>comparingDouble(r -> r.camDistance).reversed(); + + private void buildCommandBuffer() { + this.commandClientBufferBuilder.begin(); + + if(this.reverseRegions) { + ChunkCameraContext camera = this.regionCamera; + for (ChunkRegion region : this.pendingBatches) { + float x = camera.getChunkModelOffset(region.getCenterBlockX(), camera.blockOriginX, camera.originX); + float y = camera.getChunkModelOffset(region.getCenterBlockY(), camera.blockOriginY, camera.originY); + float z = camera.getChunkModelOffset(region.getCenterBlockZ(), camera.blockOriginZ, camera.originZ); + region.camDistance = x * x + y * y + z * z; + } + + this.pendingBatches.sort(REGION_REVERSER); + } + + for (ChunkRegion region : this.pendingBatches) { + ChunkDrawCallBatcher batcher = region.getDrawBatcher(); + batcher.end(); + + this.commandClientBufferBuilder.pushCommandBuffer(batcher); + } + + this.commandClientBufferBuilder.end(); + } + + private void setupUploadBatches(Iterator> renders) { + while (renders.hasNext()) { + ChunkBuildResult result = renders.next(); + + if(result == null) { + continue; + } + + ChunkRenderContainer render = result.render; + + ChunkRegion region = this.bufferManager.getRegion(render.getChunkX(), render.getChunkY(), render.getChunkZ()); + + if (region == null) { + if (result.data.getMeshSize() <= 0) { + render.setData(result.data); + continue; + } + + region = this.bufferManager.getOrCreateRegion(render.getChunkX(), render.getChunkY(), render.getChunkZ()); + } + + ObjectArrayList> uploadQueue = region.getUploadQueue(); + + if (uploadQueue.isEmpty()) { + this.pendingUploads.enqueue(region); + } + + uploadQueue.add(result); + } + } + + private void setupDrawBatches(CommandList commandList, ChunkRenderListIterator it, ChunkCameraContext camera) { + this.uniformBufferBuilder.reset(); + this.regionCamera = camera; + + int drawCount = 0; + + while (it.hasNext()) { + MultidrawGraphicsState state = it.getGraphicsState(); + int visible = it.getVisibleFaces(); + + int index = drawCount++; + float x = camera.getChunkModelOffset(state.getX(), camera.blockOriginX, camera.originX); + float y = camera.getChunkModelOffset(state.getY(), camera.blockOriginY, camera.originY); + float z = camera.getChunkModelOffset(state.getZ(), camera.blockOriginZ, camera.originZ); + + this.uniformBufferBuilder.pushChunkDrawParams(x, y, z); + + ChunkRegion region = state.getRegion(); + ChunkDrawCallBatcher batch = region.getDrawBatcher(); + + if (!batch.isBuilding()) { + batch.begin(); + + this.pendingBatches.add(region); + } + + int mask = 0b1; + + for (int i = 0; i < ModelQuadFacing.COUNT; i++) { + if ((visible & mask) != 0) { + final long part = state.getModelPart(i); + + batch.addIndirectDrawCall(BufferSlice.unpackStart(part), BufferSlice.unpackLength(part), index, 1); + } + + mask <<= 1; + } + + it.advance(); + } + + commandList.uploadData(this.uniformBuffer, this.uniformBufferBuilder.getBuffer()); + } + + private static int getUploadQueuePayloadSize(List> queue) { + int size = 0; + + for (ChunkBuildResult result : queue) { + size += result.data.getMeshSize(); + } + + return size; + } + + @Override + public void delete() { + super.delete(); + + try (CommandList commands = RenderDevice.INSTANCE.createCommandList()) { + commands.deleteBuffer(this.uploadBuffer); + commands.deleteBuffer(this.uniformBuffer); + + if (this.commandBuffer != null) { + commands.deleteBuffer(this.commandBuffer); + } + } + + this.bufferManager.delete(); + + this.commandClientBufferBuilder.delete(); + this.uniformBufferBuilder.delete(); + } + + @Override + public Class getGraphicsStateType() { + return MultidrawGraphicsState.class; + } + + public static boolean isSupported(boolean disableDriverBlacklist) { + if (!disableDriverBlacklist && isKnownBrokenIntelDriver()) { + return false; + } + + return GlFunctions.isVertexArraySupported() && + GlFunctions.isBufferCopySupported() && + GlFunctions.isIndirectMultiDrawSupported() && + GlFunctions.isInstancedArraySupported(); + } + + // https://www.intel.com/content/www/us/en/support/articles/000005654/graphics.html + private static final Pattern INTEL_BUILD_MATCHER = Pattern.compile("(\\d.\\d.\\d) - Build (\\d+).(\\d+).(\\d+).(\\d+)"); + + private static final String INTEL_VENDOR_NAME = "Intel"; + + /** + * Determines whether or not the current OpenGL renderer is an integrated Intel GPU on Windows. + * These drivers on Windows are known to fail when using command buffers. + */ + private static boolean isWindowsIntelDriver() { + // We only care about Windows + // The open-source drivers on Linux are not known to have driver bugs with indirect command buffers + if (Util.getOSType() != Util.EnumOS.WINDOWS) { + return false; + } + + // Check to see if the GPU vendor is Intel + return Objects.equals(GL11.glGetString(GL11.GL_VENDOR), INTEL_VENDOR_NAME); + } + + /** + * Determines whether or not the current OpenGL renderer is an old integrated Intel GPU (prior to Skylake/Gen8) on + * Windows. These drivers on Windows are unsupported and known to create significant trouble with the multi-draw + * renderer. + */ + private static boolean isKnownBrokenIntelDriver() { + if (!isWindowsIntelDriver()) { + return false; + } + + final String version = GL11.glGetString(GL11.GL_VERSION); + + // The returned version string may be null in the case of an error + if (version == null) { + return false; + } + + Matcher matcher = INTEL_BUILD_MATCHER.matcher(version); + + // If the version pattern doesn't match, assume we're dealing with something special + if (!matcher.matches()) { + return false; + } + + // Anything with a major build of >=100 is GPU Gen8 or newer + // The fourth group is the major build number + return Integer.parseInt(matcher.group(4)) < 100; + } + + @Override + public String getRendererName() { + return "Multidraw"; + } + + @Override + public List getDebugStrings() { + List list = new ArrayList<>(); + list.add(String.format("Active Buffers: %s", this.bufferManager.getAllocatedRegionCount())); + list.add(String.format("Submission Mode: %s", this.commandBuffer != null ? EnumChatFormatting.AQUA + "Buffer" : EnumChatFormatting.LIGHT_PURPLE + "Client Memory")); + + return list; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawGraphicsState.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawGraphicsState.java new file mode 100644 index 000000000..e4ebbdb2c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawGraphicsState.java @@ -0,0 +1,53 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import me.jellysquid.mods.sodium.client.gl.arena.GlBufferSegment; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.region.ChunkRegion; + +import java.util.Map; + +public class MultidrawGraphicsState extends ChunkGraphicsState { + private final ChunkRegion region; + + private final GlBufferSegment segment; + private final long[] parts; + + public MultidrawGraphicsState(ChunkRenderContainer container, ChunkRegion region, GlBufferSegment segment, ChunkMeshData meshData, GlVertexFormat vertexFormat) { + super(container); + + this.region = region; + this.segment = segment; + + this.parts = new long[ModelQuadFacing.COUNT]; + + for (Map.Entry entry : meshData.getSlices()) { + ModelQuadFacing facing = entry.getKey(); + BufferSlice slice = entry.getValue(); + + int start = (segment.getStart() + slice.start) / vertexFormat.getStride(); + int count = slice.len / vertexFormat.getStride(); + + this.parts[facing.ordinal()] = BufferSlice.pack(start, count); + } + } + + @Override + public void delete(CommandList commandList) { + this.segment.delete(); + } + + public ChunkRegion getRegion() { + return this.region; + } + + public long getModelPart(int facing) { + return this.parts[facing]; + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/StructBuffer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/StructBuffer.java new file mode 100644 index 000000000..89399cd56 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/StructBuffer.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; + +import org.lwjgl.BufferUtils; +import org.lwjgl.MemoryUtil; + +import java.nio.ByteBuffer; + +public abstract class StructBuffer { + protected ByteBuffer buffer; + + protected final int stride; + + protected StructBuffer(int bytes, int stride) { + this.buffer = BufferUtils.createByteBuffer(bytes * stride); + this.stride = stride; + } + + public ByteBuffer getBuffer() { + return this.buffer; + } + + public long getBufferAddress() { + return MemoryUtil.getAddress(this.buffer); + } + + public void delete() { + // no-op, because Java direct buffers free themselves + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkOneshotGraphicsState.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkOneshotGraphicsState.java new file mode 100644 index 000000000..29fc51d09 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkOneshotGraphicsState.java @@ -0,0 +1,109 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.oneshot; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlPrimitiveType; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlTessellation; +import me.jellysquid.mods.sodium.client.gl.tessellation.TessellationBinding; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.sodium.IrisChunkShaderBindingPoints; + +import java.util.Arrays; +import java.util.Map; + +public class ChunkOneshotGraphicsState extends ChunkGraphicsState { + private final GlMutableBuffer vertexBuffer; + + protected GlTessellation tessellation; + + private final long[] parts; + + protected ChunkOneshotGraphicsState(RenderDevice device, ChunkRenderContainer container) { + super(container); + + this.parts = new long[ModelQuadFacing.COUNT]; + + try (CommandList commands = device.createCommandList()) { + this.vertexBuffer = commands.createMutableBuffer(GlBufferUsage.GL_STATIC_DRAW); + } + } + + public long getModelPart(int facing) { + return this.parts[facing]; + } + + protected void setModelPart(ModelQuadFacing facing, long slice) { + this.parts[facing.ordinal()] = slice; + } + + protected void setupModelParts(ChunkMeshData meshData, GlVertexFormat vertexFormat) { + int stride = vertexFormat.getStride(); + + Arrays.fill(this.parts, 0L); + + for (Map.Entry entry : meshData.getSlices()) { + ModelQuadFacing facing = entry.getKey(); + BufferSlice slice = entry.getValue(); + + this.setModelPart(facing, BufferSlice.pack(slice.start / stride, slice.len / stride)); + } + } + + @Override + public void delete(CommandList commandList) { + commandList.deleteBuffer(this.vertexBuffer); + } + + public void upload(CommandList commandList, ChunkMeshData meshData) { + VertexData vertexData = meshData.takeVertexData(); + + commandList.uploadData(this.vertexBuffer, vertexData); + + GlVertexFormat vertexFormat = (GlVertexFormat) vertexData.format; + + this.tessellation = commandList.createTessellation(GlPrimitiveType.QUADS, new TessellationBinding[] { + new TessellationBinding(this.vertexBuffer, getBindings(vertexFormat), false) + }); + + this.setupModelParts(meshData, vertexData.format); + + vertexData.buffer.limit(vertexData.buffer.capacity()); + vertexData.buffer.position(0); + this.setTranslucencyData(vertexData.buffer); + } + + private GlVertexAttributeBinding[] getBindings(GlVertexFormat vertexFormat) { + if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { + return new GlVertexAttributeBinding[] { + new GlVertexAttributeBinding(ChunkShaderBindingPoints.POSITION, vertexFormat.getAttribute(ChunkMeshAttribute.POSITION)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.COLOR, vertexFormat.getAttribute(ChunkMeshAttribute.COLOR)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.TEX_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.TEXTURE)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.LIGHT_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.LIGHT)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.BLOCK_ID, vertexFormat.getAttribute(ChunkMeshAttribute.BLOCK_ID)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.MID_TEX_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.MID_TEX_COORD)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.TANGENT, vertexFormat.getAttribute(ChunkMeshAttribute.TANGENT)), + new GlVertexAttributeBinding(IrisChunkShaderBindingPoints.NORMAL, vertexFormat.getAttribute(ChunkMeshAttribute.NORMAL)) + }; + } else { + return new GlVertexAttributeBinding[] { + new GlVertexAttributeBinding(ChunkShaderBindingPoints.POSITION, vertexFormat.getAttribute(ChunkMeshAttribute.POSITION)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.COLOR, vertexFormat.getAttribute(ChunkMeshAttribute.COLOR)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.TEX_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.TEXTURE)), + new GlVertexAttributeBinding(ChunkShaderBindingPoints.LIGHT_COORD, vertexFormat.getAttribute(ChunkMeshAttribute.LIGHT)) }; + } + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkRenderBackendOneshot.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkRenderBackendOneshot.java new file mode 100644 index 000000000..63d2c09cc --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/oneshot/ChunkRenderBackendOneshot.java @@ -0,0 +1,133 @@ +package me.jellysquid.mods.sodium.client.render.chunk.backends.oneshot; + +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.DrawCommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.gl.util.GlMultiDrawBatch; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkCameraContext; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.lists.ChunkRenderListIterator; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkRenderShaderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; +import org.lwjgl.opengl.GL20; + +import java.util.Iterator; + +public class ChunkRenderBackendOneshot extends ChunkRenderShaderBackend { + private final GlMultiDrawBatch batch = new GlMultiDrawBatch(ModelQuadFacing.COUNT); + + public ChunkRenderBackendOneshot(ChunkVertexType vertexType) { + super(vertexType); + } + + @Override + public void upload(CommandList commandList, Iterator> queue) { + while (queue.hasNext()) { + ChunkBuildResult result = queue.next(); + + ChunkRenderContainer render = result.render; + ChunkRenderData data = result.data; + + for (BlockRenderPass pass : result.passesToUpload) { + ChunkOneshotGraphicsState state = render.getGraphicsState(pass); + ChunkMeshData mesh = data.getMesh(pass); + + if (mesh.hasVertexData()) { + if (state == null) { + state = new ChunkOneshotGraphicsState(RenderDevice.INSTANCE, render); + } + + state.upload(commandList, mesh); + // For code simplicity, ChunkOneshotGraphicsState stores the buffer unconditionally + // Reset it here if the pass isn't translucent, as we don't want to store useless + // buffers + if(!pass.isTranslucent()) + state.setTranslucencyData(null); + } else { + if (state != null) { + state.delete(commandList); + } + + state = null; + } + + render.setGraphicsState(pass, state); + } + + render.setData(data); + } + } + + @Override + public void render(CommandList commandList, ChunkRenderListIterator it, ChunkCameraContext camera) { + while (it.hasNext()) { + ChunkOneshotGraphicsState state = it.getGraphicsState(); + int visibleFaces = it.getVisibleFaces(); + + this.buildBatch(state, visibleFaces); + + if (this.batch.isBuilding()) { + this.prepareDrawBatch(camera, state); + this.drawBatch(commandList, state); + } + + it.advance(); + } + } + + protected void prepareDrawBatch(ChunkCameraContext camera, ChunkOneshotGraphicsState state) { + final float modelX = camera.getChunkModelOffset(state.getX(), camera.blockOriginX, camera.originX); + final float modelY = camera.getChunkModelOffset(state.getY(), camera.blockOriginY, camera.originY); + final float modelZ = camera.getChunkModelOffset(state.getZ(), camera.blockOriginZ, camera.originZ); + + GL20.glVertexAttrib4f(ChunkShaderBindingPoints.MODEL_OFFSET.getGenericAttributeIndex(), modelX, modelY, modelZ, 0.0F); + } + + protected void buildBatch(ChunkOneshotGraphicsState state, int visibleFaces) { + GlMultiDrawBatch batch = this.batch; + batch.begin(); + + for (int i = 0; i < ModelQuadFacing.COUNT; i++) { + if ((visibleFaces & (1 << i)) == 0) { + continue; + } + + long part = state.getModelPart(i); + batch.addChunkRender(BufferSlice.unpackStart(part), BufferSlice.unpackLength(part)); + } + } + + protected void drawBatch(CommandList commandList, ChunkOneshotGraphicsState state) { + this.batch.end(); + + if (!batch.isEmpty()) { + try (DrawCommandList drawCommandList = commandList.beginTessellating(state.tessellation)) { + drawCommandList.multiDrawArrays(this.batch.getIndicesBuffer(), this.batch.getLengthBuffer()); + } + } + } + + @Override + public void delete() { + super.delete(); + + this.batch.delete(); + } + + @Override + public Class getGraphicsStateType() { + return ChunkOneshotGraphicsState.class; + } + + @Override + public String getRendererName() { + return "Oneshot"; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBufferSorter.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBufferSorter.java new file mode 100644 index 000000000..00944fa28 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBufferSorter.java @@ -0,0 +1,184 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile; + +import com.google.common.primitives.Floats; +import it.unimi.dsi.fastutil.ints.IntArrays; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.hfp.HFPModelVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.sfp.SFPModelVertexType; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.BitSet; + +public class ChunkBufferSorter { + + private static final Class OCULUS_VERTEX_TYPE; + + static { + Class clz; + try { + clz = Class.forName("net.coderbot.iris.compat.sodium.impl.vertex_format.terrain_xhfp.XHFPModelVertexType"); + } catch(Throwable e) { + clz = null; + } + OCULUS_VERTEX_TYPE = clz; + } + + public static void sortStandardFormat(ChunkVertexType vertexType, ByteBuffer buffer, int bufferLen, float x, float y, float z) { + boolean isCompact; + + if(vertexType.getClass() == HFPModelVertexType.class || vertexType.getClass() == OCULUS_VERTEX_TYPE) { + isCompact = true; + } else if(vertexType.getClass() == SFPModelVertexType.class) { + isCompact = false; + } else + return; // ignore unsupported vertex types to avoid corruption + + // Quad stride by Float size + int quadStride = vertexType.getBufferVertexFormat().getStride(); + + int quadStart = ((Buffer)buffer).position(); + int quadCount = bufferLen/quadStride/4; + + float[] distanceArray = new float[quadCount]; + int[] indicesArray = new int[quadCount]; + + if(isCompact) { + ShortBuffer shortBuffer = buffer.asShortBuffer(); + int vertexSizeShort = quadStride / 2; + for (int quadIdx = 0; quadIdx < quadCount; ++quadIdx) { + distanceArray[quadIdx] = getDistanceSqHFP(shortBuffer, x, y, z, vertexSizeShort, quadStart + (quadIdx * quadStride * 2)); + indicesArray[quadIdx] = quadIdx; + } + } else { + FloatBuffer floatBuffer = buffer.asFloatBuffer(); + int vertexSizeInteger = quadStride / 4; + for (int quadIdx = 0; quadIdx < quadCount; ++quadIdx) { + distanceArray[quadIdx] = getDistanceSqSFP(floatBuffer, x, y, z, vertexSizeInteger, quadStart + (quadIdx * quadStride)); + indicesArray[quadIdx] = quadIdx; + } + } + + IntArrays.mergeSort(indicesArray, (a, b) -> Floats.compare(distanceArray[b], distanceArray[a])); + + rearrangeQuads(buffer, indicesArray, quadStride, quadStart); + } + + private static void rearrangeQuads(ByteBuffer quadBuffer, int[] indicesArray, int quadStride, int quadStart) { + FloatBuffer floatBuffer = quadBuffer.asFloatBuffer(); + BitSet bits = new BitSet(); + + FloatBuffer tmp = FloatBuffer.allocate(quadStride); + + for (int l = bits.nextClearBit(0); l < indicesArray.length; l = bits.nextClearBit(l + 1)) { + int m = indicesArray[l]; + + if (m != l) { + sliceQuad(floatBuffer, m, quadStride, quadStart); + ((Buffer)tmp).clear(); + tmp.put(floatBuffer); + + int n = m; + + for (int o = indicesArray[m]; n != l; o = indicesArray[o]) { + sliceQuad(floatBuffer, o, quadStride, quadStart); + FloatBuffer floatBuffer3 = floatBuffer.slice(); + + sliceQuad(floatBuffer, n, quadStride, quadStart); + floatBuffer.put(floatBuffer3); + + bits.set(n); + n = o; + } + + sliceQuad(floatBuffer, l, quadStride, quadStart); + ((Buffer)tmp).flip(); + + floatBuffer.put(tmp); + } + + bits.set(l); + } + } + + private static void sliceQuad(FloatBuffer floatBuffer, int quadIdx, int quadStride, int quadStart) { + int base = quadStart + (quadIdx * quadStride); + + ((Buffer)floatBuffer).limit(base + quadStride); + ((Buffer)floatBuffer).position(base); + } + + private static float getDistanceSqSFP(FloatBuffer buffer, float xCenter, float yCenter, float zCenter, int stride, int start) { + int vertexBase = start; + float x1 = buffer.get(vertexBase); + float y1 = buffer.get(vertexBase + 1); + float z1 = buffer.get(vertexBase + 2); + + //System.out.println("camera: " + xCenter + "," + yCenter + "," + zCenter); + //System.out.println("buffer1: " + x1 + "," + y1 + "," + z1); + + vertexBase += stride; + float x2 = buffer.get(vertexBase); + float y2 = buffer.get(vertexBase + 1); + float z2 = buffer.get(vertexBase + 2); + //System.out.println("buffer2: " + x2 + "," + y2 + "," + z2); + + vertexBase += stride; + float x3 = buffer.get(vertexBase); + float y3 = buffer.get(vertexBase + 1); + float z3 = buffer.get(vertexBase + 2); + //System.out.println("buffer3: " + x3 + "," + y3 + "," + z3); + + vertexBase += stride; + float x4 = buffer.get(vertexBase); + float y4 = buffer.get(vertexBase + 1); + float z4 = buffer.get(vertexBase + 2); + //System.out.println("buffer4: " + x4 + "," + y4 + "," + z4); + + float xDist = ((x1 + x2 + x3 + x4) * 0.25F) - xCenter; + float yDist = ((y1 + y2 + y3 + y4) * 0.25F) - yCenter; + float zDist = ((z1 + z2 + z3 + z4) * 0.25F) - zCenter; + + return (xDist * xDist) + (yDist * yDist) + (zDist * zDist); + } + + private static float normalizeShort(short s) { + return (float)Short.toUnsignedInt(s) / 2048.0f; + } + + private static float getDistanceSqHFP(ShortBuffer buffer, float xCenter, float yCenter, float zCenter, int stride, int start) { + int vertexBase = start; + float x1 = normalizeShort(buffer.get(vertexBase)); + float y1 = normalizeShort(buffer.get(vertexBase + 1)); + float z1 = normalizeShort(buffer.get(vertexBase + 2)); + + //System.out.println("camera: " + xCenter + "," + yCenter + "," + zCenter); + //System.out.println("buffer1: " + x1 + "," + y1 + "," + z1); + + vertexBase += stride; + float x2 = normalizeShort(buffer.get(vertexBase)); + float y2 = normalizeShort(buffer.get(vertexBase + 1)); + float z2 = normalizeShort(buffer.get(vertexBase + 2)); + //System.out.println("buffer2: " + x2 + "," + y2 + "," + z2); + + vertexBase += stride; + float x3 = normalizeShort(buffer.get(vertexBase)); + float y3 = normalizeShort(buffer.get(vertexBase + 1)); + float z3 = normalizeShort(buffer.get(vertexBase + 2)); + //System.out.println("buffer3: " + x3 + "," + y3 + "," + z3); + + vertexBase += stride; + float x4 = normalizeShort(buffer.get(vertexBase)); + float y4 = normalizeShort(buffer.get(vertexBase + 1)); + float z4 = normalizeShort(buffer.get(vertexBase + 2)); + //System.out.println("buffer4: " + x4 + "," + y4 + "," + z4); + + float xDist = ((x1 + x2 + x3 + x4) * 0.25F) - xCenter; + float yDist = ((y1 + y2 + y3 + y4) * 0.25F) - yCenter; + float zDist = ((z1 + z2 + z3 + z4) * 0.25F) - zCenter; + + return (xDist * xDist) + (yDist * yDist) + (zDist * zDist); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java new file mode 100644 index 000000000..8705c061b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java @@ -0,0 +1,169 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferBuilder; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.compile.buffers.BakedChunkModelBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.buffers.ChunkModelBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.buffers.ChunkModelVertexTransformer; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkModelOffset; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.sodium.block_context.BlockContextHolder; +import net.coderbot.iris.sodium.block_context.ChunkBuildBuffersExt; +import net.coderbot.iris.sodium.block_context.ContextAwareVertexWriter; +import net.minecraft.block.Block; +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * A collection of temporary buffers for each worker thread which will be used to build chunk meshes for given render + * passes. This makes a best-effort attempt to pick a suitable size for each scratch buffer, but will never try to + * shrink a buffer. + */ +public class ChunkBuildBuffers implements ChunkBuildBuffersExt { + private final ChunkModelBuffers[] delegates; + private final VertexBufferBuilder[][] buffersByLayer; + @Getter + private final ChunkVertexType vertexType; + + private final ChunkModelOffset offset; + + private BlockContextHolder iris$contextHolder; + + private static final int EXPECTED_BUFFER_SIZE = 2097152; + + public ChunkBuildBuffers(ChunkVertexType vertexType) { + this.vertexType = vertexType; + + this.delegates = new ChunkModelBuffers[BlockRenderPass.COUNT]; + this.buffersByLayer = new VertexBufferBuilder[BlockRenderPass.COUNT][ModelQuadFacing.COUNT]; + + this.offset = new ChunkModelOffset(); + + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + final int passId = pass.ordinal(); + + final VertexBufferBuilder[] buffers = this.buffersByLayer[passId]; + + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + buffers[facing.ordinal()] = new VertexBufferBuilder(vertexType.getBufferVertexFormat(), EXPECTED_BUFFER_SIZE / ModelQuadFacing.COUNT); + } + } + + if(AngelicaConfig.enableIris) { + final Object2IntMap blockMatches = BlockRenderingSettings.INSTANCE.getBlockMatches(); + + if (blockMatches != null) { + this.iris$contextHolder = new BlockContextHolder(blockMatches); + } else { + this.iris$contextHolder = new BlockContextHolder(); + } + } + } + + public void init(ChunkRenderData.Builder renderData) { + for (int i = 0; i < this.buffersByLayer.length; i++) { + ChunkModelVertexTransformer[] writers = new ChunkModelVertexTransformer[ModelQuadFacing.COUNT]; + + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + final ModelVertexSink sink = this.vertexType.createBufferWriter(this.buffersByLayer[i][facing.ordinal()], SodiumClientMod.isDirectMemoryAccessEnabled()); + if (AngelicaConfig.enableIris && sink instanceof ContextAwareVertexWriter) { + ((ContextAwareVertexWriter) sink).iris$setContextHolder(iris$contextHolder); + } + writers[facing.ordinal()] = new ChunkModelVertexTransformer(sink, this.offset); + } + + this.delegates[i] = new BakedChunkModelBuffers(writers, renderData); + } + } + + /** + * Return the {@link ChunkModelVertexTransformer} for the given {@link BlockRenderPass} as mapped by the + * {@link BlockRenderPassManager} for this render context. + */ + public ChunkModelBuffers get(BlockRenderPass pass) { + return this.delegates[pass.ordinal()]; + } + + /** + * Creates immutable baked chunk meshes from all non-empty scratch buffers and resets the state of all mesh + * builders. This is used after all blocks have been rendered to pass the finished meshes over to the graphics card. + */ + public ChunkMeshData createMesh(BlockRenderPass pass, float x, float y, float z, boolean sortTranslucent) { + VertexBufferBuilder[] builders = this.buffersByLayer[pass.ordinal()]; + + ChunkMeshData meshData = new ChunkMeshData(); + int bufferLen = 0; + + for (int facingId = 0; facingId < builders.length; facingId++) { + VertexBufferBuilder builder = builders[facingId]; + + if (builder == null || builder.isEmpty()) { + continue; + } + + int start = bufferLen; + int size = builder.getSize(); + + meshData.setModelSlice(ModelQuadFacing.VALUES[facingId], new BufferSlice(start, size)); + + bufferLen += size; + } + + if (bufferLen <= 0) { + return null; + } + + ByteBuffer buffer = BufferUtils.createByteBuffer(bufferLen); + + for (Map.Entry entry : meshData.getSlices()) { + BufferSlice slice = entry.getValue(); + buffer.position(slice.start); + + VertexBufferBuilder builder = this.buffersByLayer[pass.ordinal()][entry.getKey().ordinal()]; + builder.copyInto(buffer); + } + + buffer.flip(); + + if (sortTranslucent && pass.isTranslucent()) { + ChunkBufferSorter.sortStandardFormat(vertexType, buffer, bufferLen, x, y, z); + } + + meshData.setVertexData(new VertexData(buffer, this.vertexType.getCustomVertexFormat())); + + return meshData; + } + + public void setRenderOffset(int x, int y, int z) { + this.offset.set(x, y, z); + } + + // Iris Compat + public void iris$setLocalPos(int localPosX, int localPosY, int localPosZ) { + if(!AngelicaConfig.enableIris) return; + this.iris$contextHolder.setLocalPos(localPosX, localPosY, localPosZ); + } + + public void iris$setMaterialId(Block block, short renderType) { + if(!AngelicaConfig.enableIris) return; + this.iris$contextHolder.set(block, renderType); + } + + public void iris$resetBlockContext() { + if(!AngelicaConfig.enableIris) return; + this.iris$contextHolder.reset(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildResult.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildResult.java new file mode 100644 index 000000000..bdccb56d0 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildResult.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile; + +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; + +/** + * The result of a chunk rebuild task which contains any and all data that needs to be processed or uploaded on + * the main thread. If a task is cancelled after finishing its work and not before the result is processed, the result + * will instead be discarded. + */ +public class ChunkBuildResult { + public final ChunkRenderContainer render; + public final ChunkRenderData data; + public BlockRenderPass[] passesToUpload; + + public ChunkBuildResult(ChunkRenderContainer render, ChunkRenderData data) { + this.render = render; + this.data = data; + this.passesToUpload = BlockRenderPass.VALUES; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java new file mode 100644 index 000000000..cb2335ff9 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java @@ -0,0 +1,414 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile; + +import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.tasks.ChunkRenderBuildTask; +import me.jellysquid.mods.sodium.client.render.chunk.tasks.ChunkRenderEmptyBuildTask; +import me.jellysquid.mods.sodium.client.render.chunk.tasks.ChunkRenderRebuildTask; +import me.jellysquid.mods.sodium.client.render.chunk.tasks.ChunkRenderTranslucencySortTask; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheLocal; +import me.jellysquid.mods.sodium.client.util.task.CancellationSource; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import me.jellysquid.mods.sodium.client.world.cloned.ChunkRenderContext; +import me.jellysquid.mods.sodium.client.world.cloned.ClonedChunkSectionCache; +import me.jellysquid.mods.sodium.common.util.collections.DequeDrain; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.util.MathHelper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.joml.Vector3d; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ChunkBuilder { + /** + * The maximum number of jobs that can be queued for a given worker thread. + */ + private static final int TASK_QUEUE_LIMIT_PER_WORKER = 2; + + private static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); + + private final Deque> buildQueue = new ConcurrentLinkedDeque<>(); + private final Deque> uploadQueue = new ConcurrentLinkedDeque<>(); + + private final Object jobNotifier = new Object(); + + private final AtomicBoolean running = new AtomicBoolean(false); + private final List threads = new ArrayList<>(); + + private ClonedChunkSectionCache sectionCache; + + private WorldClient world; + private Vector3d cameraPosition = new Vector3d(); + + private final int limitThreads; + private final ChunkVertexType vertexType; + private final ChunkRenderBackend backend; + + public ChunkBuilder(ChunkVertexType vertexType, ChunkRenderBackend backend) { + this.vertexType = vertexType; + this.backend = backend; + this.limitThreads = getThreadCount(); + } + + /** + * Returns the remaining number of build tasks which should be scheduled this frame. If an attempt is made to + * spawn more tasks than the budget allows, it will block until resources become available. + */ + public int getSchedulingBudget() { + return Math.max(0, (this.limitThreads * TASK_QUEUE_LIMIT_PER_WORKER) - this.buildQueue.size()); + } + + /** + * Spawns a number of work-stealing threads to process results in the build queue. If the builder is already + * running, this method does nothing and exits. + */ + public void startWorkers() { + if (this.running.getAndSet(true)) { + return; + } + + if (!this.threads.isEmpty()) { + throw new IllegalStateException("Threads are still alive while in the STOPPED state"); + } + + Minecraft client = Minecraft.getMinecraft(); + + for (int i = 0; i < this.limitThreads; i++) { + ChunkBuildBuffers buffers = new ChunkBuildBuffers(this.vertexType); + ChunkRenderCacheLocal pipeline = new ChunkRenderCacheLocal(client, this.world); + + WorkerRunnable worker = new WorkerRunnable(buffers, pipeline); + + Thread thread = new Thread(worker, "Chunk Render Task Executor #" + i); + thread.setPriority(Math.max(0, Thread.NORM_PRIORITY - 2)); + thread.start(); + + this.threads.add(thread); + } + + LOGGER.info("Started {} worker threads", this.threads.size()); + } + + private boolean workersAlive() { + for (Thread thread : this.threads) { + if (thread.isAlive()) { + return true; + } + } + return false; + } + + /** + * Notifies all worker threads to stop and blocks until all workers terminate. After the workers have been shut + * down, all tasks are cancelled and the pending queues are cleared. If the builder is already stopped, this + * method does nothing and exits. + */ + public void stopWorkers() { + if (!this.running.getAndSet(false)) { + return; + } + + if (this.threads.isEmpty()) { + throw new IllegalStateException("No threads are alive but the executor is in the RUNNING state"); + } + + LOGGER.info("Stopping worker threads"); + + // Notify all worker threads to wake up, where they will then terminate + synchronized (this.jobNotifier) { + this.jobNotifier.notifyAll(); + } + + // Keep processing the main thread tasks so the workers don't block forever + AngelicaRenderQueue.managedBlock(() -> !workersAlive()); + + // Ensure every remaining thread has terminated + for (Thread thread : this.threads) { + try { + thread.join(); + } catch (InterruptedException ignored) { + } + } + + this.threads.clear(); + + // Drop any pending work queues and cancel futures + this.uploadQueue.clear(); + + for (WrappedTask job : this.buildQueue) { + job.future.cancel(true); + } + + this.buildQueue.clear(); + + this.world = null; + this.sectionCache = null; + } + + public void cleanupSectionCache() { + this.sectionCache.cleanup(); + } + + /** + * Processes all pending build task uploads using the chunk render backend. + */ + // TODO: Limit the amount of time this can take per frame + public boolean performPendingUploads() { + if (this.uploadQueue.isEmpty()) { + return false; + } + + this.backend.upload(RenderDevice.INSTANCE.createCommandList(), new DequeDrain<>(this.uploadQueue)); + + return true; + } + + public CompletableFuture> schedule(ChunkRenderBuildTask task) { + if (!this.running.get()) { + throw new IllegalStateException("Executor is stopped"); + } + + WrappedTask job = new WrappedTask<>(task); + + this.buildQueue.add(job); + + synchronized (this.jobNotifier) { + this.jobNotifier.notify(); + } + + return job.future; + } + + /** + * Sets the current camera position of the player used for task prioritization. + */ + public void setCameraPosition(double x, double y, double z) { + this.cameraPosition = new Vector3d(x, y, z); + } + + /** + * Returns the current camera position of the player used for task prioritization. + */ + public Vector3d getCameraPosition() { + return this.cameraPosition; + } + + /** + * @return True if the build queue is empty + */ + public boolean isBuildQueueEmpty() { + return this.buildQueue.isEmpty(); + } + + /** + * Initializes this chunk builder for the given world. If the builder is already running (which can happen during + * a world teleportation event), the worker threads will first be stopped and all pending tasks will be discarded + * before being started again. + * @param world The world instance + */ + public void init(WorldClient world) { + if (world == null) { + throw new NullPointerException("World is null"); + } + + this.stopWorkers(); + + this.world = world; + this.sectionCache = new ClonedChunkSectionCache(this.world); + + this.startWorkers(); + } + + /** + * Returns the "optimal" number of threads to be used for chunk build tasks. This is always at least one thread, + * but can be up to the number of available processor threads on the system. + */ + private static int getOptimalThreadCount() { + return MathHelper.clamp_int(Math.max(getMaxThreadCount() / 3, getMaxThreadCount() - 6), 1, 10); + } + + private static int getThreadCount() { + int requested = SodiumClientMod.options().performance.chunkBuilderThreads; + return requested == 0 ? getOptimalThreadCount() : Math.min(requested, getMaxThreadCount()); + } + + private static int getMaxThreadCount() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * Creates a rebuild task and defers it to the work queue. When the task is completed, it will be moved onto the + * completed uploads queued which will then be drained during the next available synchronization point with the + * main thread. + * @param render The render to rebuild + */ + public void deferRebuild(ChunkRenderContainer render) { + this.scheduleRebuildTaskAsync(render) + .thenAccept(this::enqueueUpload); + } + + /** + * Creates a rebuild task and defers it to the work queue. When the task is completed, it will be moved onto the + * completed uploads queued which will then be drained during the next available synchronization point with the + * main thread. + * @param render The render to rebuild + */ + public void deferSort(ChunkRenderContainer render) { + this.scheduleSortTaskAsync(render) + .thenAccept(this::enqueueUpload); + } + + + /** + * Enqueues the build task result to the pending result queue to be later processed during the next available + * synchronization point on the main thread. + * @param result The build task's result + */ + private void enqueueUpload(ChunkBuildResult result) { + this.uploadQueue.add(result); + } + + /** + * Schedules the rebuild task asynchronously on the worker pool, returning a future wrapping the task. + * @param render The render to rebuild + */ + public CompletableFuture> scheduleRebuildTaskAsync(ChunkRenderContainer render) { + return this.schedule(this.createRebuildTask(render)); + } + + /** + * Schedules the rebuild task asynchronously on the worker pool, returning a future wrapping the task. + * @param render The render to rebuild + */ + public CompletableFuture> scheduleSortTaskAsync(ChunkRenderContainer render) { + return this.schedule(this.createSortTask(render)); + } + + /** + * Creates a task to rebuild the geometry of a {@link ChunkRenderContainer}. + * @param render The render to rebuild + */ + private ChunkRenderBuildTask createRebuildTask(ChunkRenderContainer render) { + render.cancelRebuildTask(); + + ChunkRenderContext context = WorldSlice.prepare(this.world, render.getChunkPos(), this.sectionCache); + + if (context == null) { + return new ChunkRenderEmptyBuildTask<>(render); + } else { + return new ChunkRenderRebuildTask<>(render, context, render.getRenderOrigin()).withCameraPosition(this.cameraPosition); + } + } + + private ChunkRenderBuildTask createSortTask(ChunkRenderContainer render) { + render.cancelRebuildTask(); + + return new ChunkRenderTranslucencySortTask<>(render, render.getRenderOrigin(), this.cameraPosition); + } + + public void onChunkDataChanged(int x, int y, int z) { + this.sectionCache.invalidate(x, y, z); + } + + private class WorkerRunnable implements Runnable { + private final AtomicBoolean running = ChunkBuilder.this.running; + + // The re-useable build buffers used by this worker for building chunk meshes + private final ChunkBuildBuffers bufferCache; + + // Making this thread-local provides a small boost to performance by avoiding the overhead in synchronizing + // caches between different CPU cores + private final ChunkRenderCacheLocal cache; + + public WorkerRunnable(ChunkBuildBuffers bufferCache, ChunkRenderCacheLocal cache) { + this.bufferCache = bufferCache; + this.cache = cache; + } + + @Override + public void run() { + // Run until the chunk builder shuts down + while (this.running.get()) { + WrappedTask job = this.getNextJob(); + + // If the job is null or no longer valid, keep searching for a task + if (job == null || job.isCancelled()) { + continue; + } + + ChunkBuildResult result; + + try { + // Perform the build task with this worker's local resources and obtain the result + result = job.task.performBuild(this.cache, this.bufferCache, job); + } catch (Exception e) { + // Propagate any exception from chunk building + job.future.completeExceptionally(e); + continue; + } finally { + job.task.releaseResources(); + } + + // The result can be null if the task is cancelled + if (result != null) { + // Notify the future that the result is now available + job.future.complete(result); + } else if (!job.isCancelled()) { + // If the job wasn't cancelled and no result was produced, we've hit a bug + job.future.completeExceptionally(new RuntimeException("No result was produced by the task")); + } + } + } + + /** + * Returns the next task which this worker can work on or blocks until one becomes available. If no tasks are + * currently available, it will wait on {@link ChunkBuilder#jobNotifier} field until notified. + */ + private WrappedTask getNextJob() { + WrappedTask job = ChunkBuilder.this.buildQueue.poll(); + + if (job == null) { + synchronized (ChunkBuilder.this.jobNotifier) { + try { + ChunkBuilder.this.jobNotifier.wait(); + } catch (InterruptedException ignored) { + } + } + } + + return job; + } + } + + private static class WrappedTask implements CancellationSource { + private final ChunkRenderBuildTask task; + private final CompletableFuture> future; + + private WrappedTask(ChunkRenderBuildTask task) { + this.task = task; + this.future = new CompletableFuture<>(); + this.future.exceptionally(e -> { + LOGGER.info("Exception thrown while building chunk", e); +// e.printStackTrace(); + return null; + }); + } + + @Override + public boolean isCancelled() { + return this.future.isCancelled(); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuffers.java new file mode 100644 index 000000000..7b2e52cfc --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuffers.java @@ -0,0 +1,25 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile.buffers; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public class BakedChunkModelBuffers implements ChunkModelBuffers { + private final ModelVertexSink[] builders; + private final ChunkRenderData.Builder renderData; + + public BakedChunkModelBuffers(ModelVertexSink[] builders, ChunkRenderData.Builder renderData) { + this.builders = builders; + this.renderData = renderData; + } + + @Override + public ModelVertexSink getSink(ModelQuadFacing facing) { + return this.builders[facing.ordinal()]; + } + + @Override + public ChunkRenderData.Builder getRenderData() { + return this.renderData; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuffers.java new file mode 100644 index 000000000..a886d88c2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuffers.java @@ -0,0 +1,12 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile.buffers; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public interface ChunkModelBuffers { + ModelVertexSink getSink(ModelQuadFacing facing); + + @Deprecated + ChunkRenderData.Builder getRenderData(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelVertexTransformer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelVertexTransformer.java new file mode 100644 index 000000000..73fa02ed4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/ChunkModelVertexTransformer.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile.buffers; + +import me.jellysquid.mods.sodium.client.model.vertex.transformers.AbstractVertexTransformer; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkModelOffset; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public class ChunkModelVertexTransformer extends AbstractVertexTransformer implements ModelVertexSink { + /** + * The translation to be applied to all quads written into this mesh builder. + */ + private final ChunkModelOffset offset; + + public ChunkModelVertexTransformer(ModelVertexSink delegate, ChunkModelOffset offset) { + super(delegate); + + this.offset = offset; + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + this.delegate.writeQuad(x + this.offset.x, y + this.offset.y, z + this.offset.z, color, u, v, light); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/FallbackChunkModelBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/FallbackChunkModelBuffers.java new file mode 100644 index 000000000..2b3397d5e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/buffers/FallbackChunkModelBuffers.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.render.chunk.compile.buffers; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public class FallbackChunkModelBuffers implements ChunkModelBuffers { + public FallbackChunkModelBuffers() { + + } + + @Override + public ModelVertexSink getSink(ModelQuadFacing facing) { + return null; + } + + @Override + public ChunkRenderData.Builder getRenderData() { + return null; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkCuller.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkCuller.java new file mode 100644 index 000000000..c718516f4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkCuller.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.client.render.chunk.cull; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionData; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; + + +public interface ChunkCuller { + IntArrayList computeVisible(Camera camera, FrustumExtended frustum, int frame, boolean spectator); + + void onSectionStateChanged(int x, int y, int z, ChunkOcclusionData occlusionData); + void onSectionLoaded(int x, int y, int z, int id); + void onSectionUnloaded(int x, int y, int z); + + boolean isSectionVisible(int x, int y, int z); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkFaceFlags.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkFaceFlags.java new file mode 100644 index 000000000..6ea6b5c5c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/ChunkFaceFlags.java @@ -0,0 +1,29 @@ +package me.jellysquid.mods.sodium.client.render.chunk.cull; + +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; + +public class ChunkFaceFlags { + public static final int UP = of(ModelQuadFacing.UP); + public static final int DOWN = of(ModelQuadFacing.DOWN); + public static final int WEST = of(ModelQuadFacing.WEST); + public static final int EAST = of(ModelQuadFacing.EAST); + public static final int NORTH = of(ModelQuadFacing.NORTH); + public static final int SOUTH = of(ModelQuadFacing.SOUTH); + public static final int UNASSIGNED = of(ModelQuadFacing.UNASSIGNED); + + public static final int ALL = all(); + + public static int of(ModelQuadFacing facing) { + return 1 << facing.ordinal(); + } + + private static int all() { + int v = 0; + + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + v |= of(facing); + } + + return v; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphCuller.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphCuller.java new file mode 100644 index 000000000..ad8c891d6 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphCuller.java @@ -0,0 +1,222 @@ +package me.jellysquid.mods.sodium.client.render.chunk.cull.graph; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionData; +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import me.jellysquid.mods.sodium.client.render.chunk.cull.ChunkCuller; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import net.minecraft.block.Block; +import net.minecraft.world.World; +import net.minecraftforge.common.util.ForgeDirection; +import net.minecraft.util.MathHelper; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class ChunkGraphCuller implements ChunkCuller { + private final Long2ObjectMap nodes = new Long2ObjectOpenHashMap<>(); + + private final ChunkGraphIterationQueue visible = new ChunkGraphIterationQueue(); + private final World world; + private final int renderDistance; + + private FrustumExtended frustum; + private boolean useOcclusionCulling; + + private int activeFrame = 0; + private int centerChunkX, centerChunkY, centerChunkZ; + + public ChunkGraphCuller(World world, int renderDistance) { + this.world = world; + this.renderDistance = renderDistance; + } + + @Override + public IntArrayList computeVisible(Camera camera, FrustumExtended frustum, int frame, boolean spectator) { + this.initSearch(camera, frustum, frame, spectator); + + ChunkGraphIterationQueue queue = this.visible; + + for (int i = 0; i < queue.size(); i++) { + ChunkGraphNode node = queue.getNode(i); + short cullData = node.computeQueuePop(); + + for (ForgeDirection dir : DirectionUtil.ALL_DIRECTIONS) { + if (useOcclusionCulling && (cullData & (1 << dir.ordinal())) == 0) { + continue; + } + + ChunkGraphNode adj = node.getConnectedNode(dir); + + if (adj != null && this.isWithinRenderDistance(adj)) { + this.bfsEnqueue(node, adj, dir.getOpposite(), cullData); + } + } + } + + return this.visible.getOrderedIdList(); + } + + private boolean isWithinRenderDistance(ChunkGraphNode adj) { + int x = Math.abs(adj.getChunkX() - this.centerChunkX); + int z = Math.abs(adj.getChunkZ() - this.centerChunkZ); + + return x <= this.renderDistance && z <= this.renderDistance; + } + + private void initSearch(Camera camera, FrustumExtended frustum, int frame, boolean spectator) { + this.activeFrame = frame; + this.frustum = frustum; + this.useOcclusionCulling = true; + + this.visible.clear(); + + BlockPos origin = camera.getBlockPos(); + + int chunkX = origin.getX() >> 4; + int chunkY = origin.getY() >> 4; + int chunkZ = origin.getZ() >> 4; + + this.centerChunkX = chunkX; + this.centerChunkY = chunkY; + this.centerChunkZ = chunkZ; + + ChunkGraphNode rootNode = this.getNode(chunkX, chunkY, chunkZ); + + if (rootNode != null) { + rootNode.resetCullingState(); + rootNode.setLastVisibleFrame(frame); + + if (spectator) { + Block block = this.world.getBlock(origin.getX(), origin.getY(), origin.getZ()); + if(block.isOpaqueCube()) { + this.useOcclusionCulling = false; + } + } + + this.visible.add(rootNode); + } else { + chunkY = MathHelper.clamp_int(origin.getY() >> 4, 0, 15); + + List bestNodes = new ArrayList<>(); + + for (int x2 = -this.renderDistance; x2 <= this.renderDistance; ++x2) { + for (int z2 = -this.renderDistance; z2 <= this.renderDistance; ++z2) { + ChunkGraphNode node = this.getNode(chunkX + x2, chunkY, chunkZ + z2); + + if (node == null || node.isCulledByFrustum(frustum)) { + continue; + } + + node.resetCullingState(); + node.setLastVisibleFrame(frame); + + bestNodes.add(node); + } + } + + bestNodes.sort(Comparator.comparingDouble(node -> node.getSquaredDistance(origin))); + + for (ChunkGraphNode node : bestNodes) { + this.visible.add(node); + } + } + } + + + private void bfsEnqueue(ChunkGraphNode parent, ChunkGraphNode node, ForgeDirection flow, short parentalData) { + if (node.getLastVisibleFrame() == this.activeFrame) { + node.updateCullingState(flow, parentalData); + return; + } + node.setLastVisibleFrame(this.activeFrame); + + if (node.isCulledByFrustum(this.frustum)) { + return; + } + + node.setCullingState(parentalData); + node.updateCullingState(flow, parentalData); + + this.visible.add(node); + } + + private void connectNeighborNodes(ChunkGraphNode node) { + for (ForgeDirection dir : DirectionUtil.ALL_DIRECTIONS) { + ChunkGraphNode adj = this.findAdjacentNode(node, dir); + + if (adj != null) { + adj.setAdjacentNode(dir.getOpposite(), node); + } + + node.setAdjacentNode(dir, adj); + } + } + + private void disconnectNeighborNodes(ChunkGraphNode node) { + for (ForgeDirection dir : DirectionUtil.ALL_DIRECTIONS) { + ChunkGraphNode adj = node.getConnectedNode(dir); + + if (adj != null) { + adj.setAdjacentNode(dir.getOpposite(), null); + } + + node.setAdjacentNode(dir, null); + } + } + + private ChunkGraphNode findAdjacentNode(ChunkGraphNode node, ForgeDirection dir) { + return this.getNode(node.getChunkX() + dir.offsetX, node.getChunkY() + dir.offsetY, node.getChunkZ() + dir.offsetZ); + } + + private ChunkGraphNode getNode(int x, int y, int z) { + return this.nodes.get(ChunkSectionPos.asLong(x, y, z)); + } + + @Override + public void onSectionStateChanged(int x, int y, int z, ChunkOcclusionData occlusionData) { + ChunkGraphNode node = this.getNode(x, y, z); + + if (node != null) { + node.setOcclusionData(occlusionData); + } + } + + @Override + public void onSectionLoaded(int x, int y, int z, int id) { + ChunkGraphNode node = new ChunkGraphNode(x, y, z, id); + ChunkGraphNode prev; + + if ((prev = this.nodes.put(ChunkSectionPos.asLong(x, y, z), node)) != null) { + this.disconnectNeighborNodes(prev); + } + + this.connectNeighborNodes(node); + } + + @Override + public void onSectionUnloaded(int x, int y, int z) { + ChunkGraphNode node = this.nodes.remove(ChunkSectionPos.asLong(x, y, z)); + + if (node != null) { + this.disconnectNeighborNodes(node); + } + } + + @Override + public boolean isSectionVisible(int x, int y, int z) { + ChunkGraphNode render = this.getNode(x, y, z); + + if (render == null) { + return false; + } + + return render.getLastVisibleFrame() == this.activeFrame; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphIterationQueue.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphIterationQueue.java new file mode 100644 index 000000000..981464e11 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphIterationQueue.java @@ -0,0 +1,58 @@ +package me.jellysquid.mods.sodium.client.render.chunk.cull.graph; + +import it.unimi.dsi.fastutil.ints.IntArrayList; + +import java.util.Arrays; + +public class ChunkGraphIterationQueue { + private int[] positions; + private ChunkGraphNode[] nodes; + + private int pos; + private int capacity; + + public ChunkGraphIterationQueue() { + this(4096); + } + + public ChunkGraphIterationQueue(int capacity) { + this.positions = new int[capacity]; + this.nodes = new ChunkGraphNode[capacity]; + + this.capacity = capacity; + } + + public void add(ChunkGraphNode node) { + int i = this.pos++; + + if (i == this.capacity) { + this.resize(); + } + + this.positions[i] = node.getId(); + this.nodes[i] = node; + } + + private void resize() { + this.capacity *= 2; + + this.positions = Arrays.copyOf(this.positions, this.capacity); + this.nodes = Arrays.copyOf(this.nodes, this.capacity); + } + + public ChunkGraphNode getNode(int i) { + return this.nodes[i]; + } + + public void clear() { + this.pos = 0; + } + + public int size() { + return this.pos; + } + + public IntArrayList getOrderedIdList() { + return IntArrayList.wrap(this.positions, this.pos); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphNode.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphNode.java new file mode 100644 index 000000000..688898e9b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/cull/graph/ChunkGraphNode.java @@ -0,0 +1,184 @@ +package me.jellysquid.mods.sodium.client.render.chunk.cull.graph; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.util.math.FrustumExtended; +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import net.minecraftforge.common.util.ForgeDirection; + +public class ChunkGraphNode { + private static final long DEFAULT_VISIBILITY_DATA = calculateVisibilityData(ChunkRenderData.EMPTY.getOcclusionData()); + private static final float FRUSTUM_EPSILON = 1.0f /* block model margin */ + 0.125f /* epsilon */; + + private final ChunkGraphNode[] nodes = new ChunkGraphNode[DirectionUtil.ALL_DIRECTIONS.length]; + + private final int id; + private final int chunkX, chunkY, chunkZ; + + private int lastVisibleFrame = -1; + + private long visibilityData; + private short cullingState; + + public ChunkGraphNode(int chunkX, int chunkY, int chunkZ, int id) { + this.chunkX = chunkX; + this.chunkY = chunkY; + this.chunkZ = chunkZ; + this.id = id; + + this.visibilityData = DEFAULT_VISIBILITY_DATA; + } + + public ChunkGraphNode getConnectedNode(ForgeDirection dir) { + return this.nodes[dir.ordinal()]; + } + + public void setLastVisibleFrame(int frame) { + this.lastVisibleFrame = frame; + } + + public int getLastVisibleFrame() { + return this.lastVisibleFrame; + } + + public int getChunkX() { + return this.chunkX; + } + + public int getChunkY() { + return this.chunkY; + } + + public int getChunkZ() { + return this.chunkZ; + } + + public void setAdjacentNode(ForgeDirection dir, ChunkGraphNode node) { + this.nodes[dir.ordinal()] = node; + } + + public void setOcclusionData(ChunkOcclusionData occlusionData) { + this.visibilityData = calculateVisibilityData(occlusionData); + } + + private static long calculateVisibilityData(ChunkOcclusionData occlusionData) { + long visibilityData = 0; + + for (ForgeDirection from : DirectionUtil.ALL_DIRECTIONS) { + for (ForgeDirection to : DirectionUtil.ALL_DIRECTIONS) { + if (occlusionData == null || occlusionData.isVisibleThrough(from, to)) { + visibilityData |= (1L << ((from.ordinal() << 3) + to.ordinal())); + } + } + } + + return visibilityData; + } + + //The way this works now is that the culling state contains 2 inner states + // visited directions mask, and visitable direction mask + //On graph start, the root node(s) have the visit and visitable masks set to all visible + // when a chunk section is popped off the queue, the visited direction mask is anded with the + // visitable direction mask to return a bitfield containing what directions the graph can flow too + //When a chunk is visited in the graph the inbound direction is masked off from the visited direction mask + // and the visitable direction mask is updated (ored) with the visibilityData of the inbound direction + //When a chunk hasnt been visited before, it uses the parents data as the initial visited direction mask + + public short computeQueuePop() { + short retVal = (short) (cullingState & (((cullingState >> 8) & 0xFF) | 0xFF00)); + cullingState = 0; + return retVal; + } + + public void updateCullingState(ForgeDirection flow, short parent) { + int inbound = flow.ordinal(); + this.cullingState |= (visibilityData >> (inbound<<3)) & 0xFF; + this.cullingState &= ~(1 << (inbound + 8)); + //NOTE: this isnt strictly needed, due to the properties provided from the bfs search (never backtracking), + // but just incase/better readability/understandability + this.cullingState &= parent|0x00FF; + } + + public void setCullingState(short parent) { + this.cullingState = (short) (parent & 0xFF00); + } + + public void resetCullingState() { + this.cullingState = -1; + } + + public int getId() { + return this.id; + } + + public boolean isCulledByFrustum(FrustumExtended frustum) { + float x = this.getOriginX(); + float y = this.getOriginY(); + float z = this.getOriginZ(); + + return !frustum.fastAabbTest(x - FRUSTUM_EPSILON, y - FRUSTUM_EPSILON, z - FRUSTUM_EPSILON, + x + 16.0f + FRUSTUM_EPSILON, y + 16.0f + FRUSTUM_EPSILON, z + 16.0f + FRUSTUM_EPSILON); + } + + /** + * @return The x-coordinate of the origin position of this chunk render + */ + public int getOriginX() { + return this.chunkX << 4; + } + + /** + * @return The y-coordinate of the origin position of this chunk render + */ + public int getOriginY() { + return this.chunkY << 4; + } + + /** + * @return The z-coordinate of the origin position of this chunk render + */ + public int getOriginZ() { + return this.chunkZ << 4; + } + + /** + * @return The squared distance from the center of this chunk in the world to the center of the block position + * given by {@param pos} + */ + public double getSquaredDistance(BlockPos pos) { + return this.getSquaredDistance(pos.x + 0.5D, pos.y + 0.5D, pos.z + 0.5D); + } + + /** + * @return The x-coordinate of the center position of this chunk render + */ + private double getCenterX() { + return this.getOriginX() + 8.0D; + } + + /** + * @return The y-coordinate of the center position of this chunk render + */ + private double getCenterY() { + return this.getOriginY() + 8.0D; + } + + /** + * @return The z-coordinate of the center position of this chunk render + */ + private double getCenterZ() { + return this.getOriginZ() + 8.0D; + } + + /** + * @return The squared distance from the center of this chunk in the world to the given position + */ + public double getSquaredDistance(double x, double y, double z) { + double xDist = x - this.getCenterX(); + double yDist = y - this.getCenterY(); + double zDist = z - this.getCenterZ(); + + return (xDist * xDist) + (yDist * yDist) + (zDist * zDist); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkMeshData.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkMeshData.java new file mode 100644 index 000000000..fe2e04031 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkMeshData.java @@ -0,0 +1,51 @@ +package me.jellysquid.mods.sodium.client.render.chunk.data; + +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; + +import java.util.EnumMap; +import java.util.Map; + +public class ChunkMeshData { + public static final ChunkMeshData EMPTY = new ChunkMeshData(); + + private final EnumMap parts = new EnumMap<>(ModelQuadFacing.class); + private VertexData vertexData; + + public void setVertexData(VertexData vertexData) { + this.vertexData = vertexData; + } + + public void setModelSlice(ModelQuadFacing facing, BufferSlice slice) { + this.parts.put(facing, slice); + } + + public VertexData takeVertexData() { + VertexData data = this.vertexData; + + if (data == null) { + throw new NullPointerException("No pending data to upload"); + } + + this.vertexData = null; + + return data; + } + + public boolean hasVertexData() { + return this.vertexData != null; + } + + public int getVertexDataSize() { + if (this.vertexData != null) { + return this.vertexData.buffer.capacity(); + } + + return 0; + } + + public Iterable> getSlices() { + return this.parts.entrySet(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderBounds.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderBounds.java new file mode 100644 index 000000000..4a06495ab --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderBounds.java @@ -0,0 +1,81 @@ +package me.jellysquid.mods.sodium.client.render.chunk.data; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; + +public class ChunkRenderBounds { + public static final ChunkRenderBounds ALWAYS_FALSE = new ChunkRenderBounds(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + public final float x1, y1, z1; + public final float x2, y2, z2; + + public ChunkRenderBounds(float x1, float y1, float z1, float x2, float y2, float z2) { + this.x1 = x1; + this.y1 = y1; + this.z1 = z1; + + this.x2 = x2; + this.y2 = y2; + this.z2 = z2; + } + + public ChunkRenderBounds(ChunkSectionPos origin) { + this.x1 = origin.getMinX(); + this.y1 = origin.getMinY(); + this.z1 = origin.getMinZ(); + + this.x2 = origin.getMaxX() + 1; + this.y2 = origin.getMaxY() + 1; + this.z2 = origin.getMaxZ() + 1; + } + + public static class Builder { + // Bit-mask of the blocks set on each axis + private int x = 0, y = 0, z = 0; + + public void addBlock(int x, int y, int z) { + // Accumulate bits on each axis for the given block position. This avoids needing to + // branch multiple times trying to determine if the bounds need to grow. The min/max + // value of each axis can later be extracted by counting the trailing/leading zeroes. + this.x |= 1 << x; + this.y |= 1 << y; + this.z |= 1 << z; + } + + public ChunkRenderBounds build(ChunkSectionPos origin) { + // If no bits were set on any axis, return the default bounds + if ((this.x | this.y | this.z) == 0) { + return new ChunkRenderBounds(origin); + } + + int x1 = origin.getMinX() + leftBound(this.x); + int x2 = origin.getMinX() + rightBound(this.x); + + int y1 = origin.getMinY() + leftBound(this.y); + int y2 = origin.getMinY() + rightBound(this.y); + + int z1 = origin.getMinZ() + leftBound(this.z); + int z2 = origin.getMinZ() + rightBound(this.z); + + return new ChunkRenderBounds( + Math.max(x1, origin.getMinX()) - 0.5f, + Math.max(y1, origin.getMinY()) - 0.5f, + Math.max(z1, origin.getMinZ()) - 0.5f, + + Math.min(x2, origin.getMaxX()) + 0.5f, + Math.min(y2, origin.getMaxY()) + 0.5f, + Math.min(z2, origin.getMaxZ()) + 0.5f + ); + } + + // Return the left-bound of the bit-masked axis + private static int leftBound(int i) { + return Integer.numberOfTrailingZeros(i); + } + + // Return the right-bound of the bit-masked axis + private static int rightBound(int i) { + return 32 - Integer.numberOfLeadingZeros(i); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderData.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderData.java new file mode 100644 index 000000000..635917b35 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/data/ChunkRenderData.java @@ -0,0 +1,201 @@ +package me.jellysquid.mods.sodium.client.render.chunk.data; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionData; +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.tileentity.TileEntity; +import net.minecraftforge.common.util.ForgeDirection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The render data for a chunk render container containing all the information about which meshes are attached, the + * block entities contained by it, and any data used for occlusion testing. + */ +public class ChunkRenderData { + public static final ChunkRenderData ABSENT = new Builder() + .build(); + public static final ChunkRenderData EMPTY = createEmptyData(); + + private Set globalTileEntities; + private List tileEntities; + + private EnumMap meshes; + + @Getter + private ChunkOcclusionData occlusionData; + @Getter + private ChunkRenderBounds bounds; + + @Getter + private List animatedSprites; + + private boolean isEmpty; + private int meshByteSize; + @Getter + private int facesWithData; + + /** + * @return True if the chunk has no renderables, otherwise false + */ + public boolean isEmpty() { + return this.isEmpty; + } + + /** + * The collection of block entities contained by this rendered chunk. + */ + public Collection getTileEntities() { + return this.tileEntities; + } + + /** + * The collection of block entities contained by this rendered chunk section which are not part of its culling + * volume. These entities should always be rendered regardless of the render being visible in the frustum. + */ + public Collection getGlobalTileEntities() { + return this.globalTileEntities; + } + + /** + * The collection of chunk meshes belonging to this render. + */ + public ChunkMeshData getMesh(BlockRenderPass pass) { + return this.meshes.get(pass); + } + + public void setMesh(BlockRenderPass pass, ChunkMeshData data) { + if (this.meshes.get(pass) == null) + throw new IllegalStateException("No mesh found"); + this.meshes.put(pass, data); + } + + public int getMeshSize() { + return this.meshByteSize; + } + + public ChunkRenderData copyAndReplaceMesh(Map replacements) { + ChunkRenderData data = new ChunkRenderData(); + data.globalTileEntities = this.globalTileEntities; + data.tileEntities = this.tileEntities; + data.occlusionData = this.occlusionData; + data.meshes = new EnumMap<>(this.meshes); + data.bounds = this.bounds; + data.animatedSprites = new ObjectArrayList<>(this.animatedSprites); + data.meshes.putAll(replacements); + + int facesWithData = 0; + int size = 0; + + for (ChunkMeshData meshData : this.meshes.values()) { + size += meshData.getVertexDataSize(); + + for (Map.Entry entry : meshData.getSlices()) { + facesWithData |= 1 << entry.getKey().ordinal(); + } + } + + data.isEmpty = this.globalTileEntities.isEmpty() && this.tileEntities.isEmpty() && facesWithData == 0; + data.meshByteSize = size; + data.facesWithData = facesWithData; + return data; + } + + public static class Builder { + private final List globalTileEntities = new ArrayList<>(); + private final List tileEntities = new ArrayList<>(); + private final Set animatedSprites = new ObjectOpenHashSet<>(); + + private final EnumMap meshes = new EnumMap<>(BlockRenderPass.class); + + private ChunkOcclusionData occlusionData; + private ChunkRenderBounds bounds = ChunkRenderBounds.ALWAYS_FALSE; + + public Builder() { + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + this.setMesh(pass, ChunkMeshData.EMPTY); + } + } + + public void setBounds(ChunkRenderBounds bounds) { + this.bounds = bounds; + } + + public void setOcclusionData(ChunkOcclusionData data) { + this.occlusionData = data; + } + + /** + * Adds a sprite to this data container for tracking. If the sprite is tickable, it will be ticked every frame + * before rendering as necessary. + * @param sprite The sprite + */ + public void addSprite(TextureAtlasSprite sprite) { + if (((ISpriteExt) sprite).isAnimation()) { + this.animatedSprites.add(sprite); + } + } + + public void setMesh(BlockRenderPass pass, ChunkMeshData data) { + this.meshes.put(pass, data); + } + + /** + * Adds a block entity to the data container. + * @param entity The block entity itself + * @param cull True if the block entity can be culled to this chunk render's volume, otherwise false + */ + public void addTileEntity(TileEntity entity, boolean cull) { + (cull ? this.tileEntities : this.globalTileEntities).add(entity); + } + + public ChunkRenderData build() { + ChunkRenderData data = new ChunkRenderData(); + data.globalTileEntities = new ObjectOpenHashSet<>(this.globalTileEntities); + data.tileEntities = this.tileEntities; + data.occlusionData = this.occlusionData; + data.meshes = this.meshes; + data.bounds = this.bounds; + data.animatedSprites = new ObjectArrayList<>(this.animatedSprites); + + int facesWithData = 0; + int size = 0; + + for (ChunkMeshData meshData : this.meshes.values()) { + size += meshData.getVertexDataSize(); + + for (Map.Entry entry : meshData.getSlices()) { + facesWithData |= 1 << entry.getKey().ordinal(); + } + } + + data.isEmpty = this.globalTileEntities.isEmpty() && this.tileEntities.isEmpty() && facesWithData == 0; + data.meshByteSize = size; + data.facesWithData = facesWithData; + + return data; + } + } + + private static ChunkRenderData createEmptyData() { + ChunkOcclusionData occlusionData = new ChunkOcclusionData(); + occlusionData.addOpenEdgeFaces(EnumSet.allOf(ForgeDirection.class)); + + Builder meshInfo = new Builder(); + meshInfo.setOcclusionData(occlusionData); + + return meshInfo.build(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkMeshAttribute.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkMeshAttribute.java new file mode 100644 index 000000000..1a4d572aa --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkMeshAttribute.java @@ -0,0 +1,14 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format; + +public enum ChunkMeshAttribute { + POSITION, + COLOR, + TEXTURE, + LIGHT, + // Iris + NORMAL, + TANGENT, + MID_TEX_COORD, + BLOCK_ID, + MID_BLOCK +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkModelOffset.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkModelOffset.java new file mode 100644 index 000000000..64b441a08 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ChunkModelOffset.java @@ -0,0 +1,11 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format; + +public class ChunkModelOffset { + public float x, y, z; + + public void set(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/DefaultModelVertexFormats.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/DefaultModelVertexFormats.java new file mode 100644 index 000000000..03e523dda --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/DefaultModelVertexFormats.java @@ -0,0 +1,9 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format; + +import me.jellysquid.mods.sodium.client.render.chunk.format.hfp.HFPModelVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.sfp.SFPModelVertexType; + +public class DefaultModelVertexFormats { + public static final HFPModelVertexType MODEL_VERTEX_HFP = new HFPModelVertexType(); + public static final SFPModelVertexType MODEL_VERTEX_SFP = new SFPModelVertexType(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexSink.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexSink.java new file mode 100644 index 000000000..91d56ec8a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexSink.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format; + +import me.jellysquid.mods.sodium.client.model.vertex.VertexSink; + +public interface ModelVertexSink extends VertexSink { + /** + * Writes a quad vertex to this sink. + * @param x The x-position of the vertex + * @param y The y-position of the vertex + * @param z The z-position of the vertex + * @param color The ABGR-packed color of the vertex + * @param u The u-texture of the vertex + * @param v The y-texture of the vertex + * @param light The packed light-map coordinates of the vertex + */ + void writeQuad(float x, float y, float z, int color, float u, float v, int light); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexUtil.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexUtil.java new file mode 100644 index 000000000..4d05c9ed9 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/ModelVertexUtil.java @@ -0,0 +1,46 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format; + +public class ModelVertexUtil { + /** + * Converts a floating point vertex position in range 0..32 to a de-normalized unsigned short. + * @param value The float in range 0..32 + * @return The resulting de-normalized unsigned short + */ + public static short denormalizeVertexPositionFloatAsShort(float value) { + // Since we're skipping the scaling formerly done in ChunkModelVertexTransformer to preserve precision, this + // method actually takes input unnormalized within the range 0..32, and expands that to 0..65536. + // TODO: Restructure things to be less arbitrary here + return (short) (value * 2048.0f); + } + + /** + * Converts a normalized floating point texture coordinate to a de-normalized unsigned short. + * @param value The normalized float + * @return The resulting de-normalized unsigned short + */ + public static short denormalizeVertexTextureFloatAsShort(float value) { + return (short) (Math.min(0.99999997F, value) * 32768.0f); + } + + /** + * This moves some work out the shader code and simplifies things a bit. In vanilla, the game encodes light map + * texture coordinates as two un-normalized unsigned shorts in the range 0..255. Using the fixed-function pipeline, + * it then applies a matrix transformation which normalizes these coordinates and applies a centering offset. This + * operation has non-zero overhead and complicates shader code a bit. + * + * To work around the problem, this function instead normalizes these light map texture coordinates and applies the + * centering offset, allowing it to be baked into the vertex data itself. + * + * @param light The light map value + * @return The light map texture coordinates as two unsigned shorts with a center offset applied + */ + public static int encodeLightMapTexCoord(int light) { + int sl = (light >> 16) & 255; + sl = (sl << 8) + 2048; + + int bl = light & 255; + bl = (bl << 8) + 2048; + + return (sl << 16) | bl; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterNio.java new file mode 100644 index 000000000..b06868e28 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterNio.java @@ -0,0 +1,43 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.hfp; + +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.render.chunk.format.DefaultModelVertexFormats; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexUtil; + +import java.nio.ByteBuffer; + +public class HFPModelVertexBufferWriterNio extends VertexBufferWriterNio implements ModelVertexSink { + public HFPModelVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, DefaultModelVertexFormats.MODEL_VERTEX_HFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + this.writeQuadInternal( + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(x), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(y), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(z), + color, + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(u), + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(v), + ModelVertexUtil.encodeLightMapTexCoord(light) + ); + } + + private void writeQuadInternal(short x, short y, short z, int color, short u, short v, int light) { + int i = this.writeOffset; + + ByteBuffer buffer = this.byteBuffer; + buffer.putShort(i, x); + buffer.putShort(i + 2, y); + buffer.putShort(i + 4, z); + buffer.putInt(i + 8, color); + buffer.putShort(i + 12, u); + buffer.putShort(i + 14, v); + buffer.putInt(i + 16, light); + + this.advance(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..e53fbbe2c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexBufferWriterUnsafe.java @@ -0,0 +1,42 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.hfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.render.chunk.format.DefaultModelVertexFormats; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexUtil; + +public class HFPModelVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements ModelVertexSink { + public HFPModelVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, DefaultModelVertexFormats.MODEL_VERTEX_HFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + this.writeQuadInternal( + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(x), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(y), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(z), + color, + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(u), + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(v), + ModelVertexUtil.encodeLightMapTexCoord(light) + ); + } + + private void writeQuadInternal(short x, short y, short z, int color, short u, short v, int light) { + long i = this.writePointer; + + CompatMemoryUtil.memPutShort(i, x); + CompatMemoryUtil.memPutShort(i + 2, y); + CompatMemoryUtil.memPutShort(i + 4, z); + CompatMemoryUtil.memPutInt(i + 8, color); + CompatMemoryUtil.memPutShort(i + 12, u); + CompatMemoryUtil.memPutShort(i + 14, v); + CompatMemoryUtil.memPutInt(i + 16, light); + + this.advance(); + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexType.java new file mode 100644 index 000000000..5bb9db6ec --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/hfp/HFPModelVertexType.java @@ -0,0 +1,67 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.hfp; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import net.coderbot.iris.sodium.vertex_format.IrisGlVertexAttributeFormat; + +/** + * Uses half-precision floating point numbers to represent position coordinates and normalized unsigned shorts for + * texture coordinates. All texel positions in the block diffuse texture atlas can be exactly mapped (including + * their centering offset), as the + */ +public class HFPModelVertexType implements ChunkVertexType { + // TODO Iris? + public static final int STRIDE = 44; // 20 + public static final GlVertexFormat VERTEX_FORMAT = + GlVertexFormat.builder(ChunkMeshAttribute.class, STRIDE) + .addElement(ChunkMeshAttribute.POSITION, 0, GlVertexAttributeFormat.UNSIGNED_SHORT, 3, false) + .addElement(ChunkMeshAttribute.COLOR, 8, GlVertexAttributeFormat.UNSIGNED_BYTE, 4, true) + .addElement(ChunkMeshAttribute.TEXTURE, 12, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, false) + .addElement(ChunkMeshAttribute.LIGHT, 16, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, true) + .addElement(ChunkMeshAttribute.MID_TEX_COORD, 20, GlVertexAttributeFormat.FLOAT, 2, false) + .addElement(ChunkMeshAttribute.TANGENT, 28, IrisGlVertexAttributeFormat.BYTE, 4, true) + .addElement(ChunkMeshAttribute.NORMAL, 32, IrisGlVertexAttributeFormat.BYTE, 3, true) + .addElement(ChunkMeshAttribute.BLOCK_ID, 36, IrisGlVertexAttributeFormat.SHORT, 2, false) + .addElement(ChunkMeshAttribute.MID_BLOCK, 40, IrisGlVertexAttributeFormat.BYTE, 3, false) + .build(); + + + public static final float MODEL_SCALE = (32.0f / 65536.0f); + public static final float TEXTURE_SCALE = (1.0f / 32768.0f); + + @Override + public ModelVertexSink createFallbackWriter(VertexConsumer consumer) { + throw new UnsupportedOperationException(); + } + + @Override + public ModelVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new HFPModelVertexBufferWriterUnsafe(buffer) : new HFPModelVertexBufferWriterNio(buffer); + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } + + @Override + public GlVertexFormat getCustomVertexFormat() { + return VERTEX_FORMAT; + } + + @Override + public float getModelScale() { + return MODEL_SCALE; + } + + @Override + public float getTextureScale() { + return TEXTURE_SCALE; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterNio.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterNio.java new file mode 100644 index 000000000..9e32a9188 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterNio.java @@ -0,0 +1,52 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.sfp; + +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.render.chunk.format.DefaultModelVertexFormats; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +import java.nio.ByteBuffer; + +public class SFPModelVertexBufferWriterNio extends VertexBufferWriterNio implements ModelVertexSink { + public SFPModelVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, DefaultModelVertexFormats.MODEL_VERTEX_SFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + int i = this.writeOffset; + + ByteBuffer buffer = this.byteBuffer; + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putInt(i + 12, color); + buffer.putFloat(i + 16, u); + buffer.putFloat(i + 20, v); + buffer.putInt(i + 24, encodeLightMapTexCoord(light)); + + this.advance(); + } + + /** + * This moves some work out the shader code and simplifies things a bit. In vanilla, the game encodes light map + * texture coordinates as two un-normalized unsigned shorts in the range 0..255. Using the fixed-function pipeline, + * it then applies a matrix transformation which normalizes these coordinates and applies a centering offset. This + * operation has non-zero overhead and complicates shader code a bit. + * + * To work around the problem, this function instead normalizes these light map texture coordinates and applies the + * centering offset, allowing it to be baked into the vertex data itself. + * + * @param light The light map value + * @return The light map texture coordinates as two unsigned shorts with a center offset applied + */ + private static int encodeLightMapTexCoord(int light) { + int sl = (light >> 16) & 255; + sl = (sl << 8) + 2048; + + int bl = light & 255; + bl = (bl << 8) + 2048; + + return (sl << 16) | bl; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterUnsafe.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..d4ca903cb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexBufferWriterUnsafe.java @@ -0,0 +1,50 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.sfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.render.chunk.format.DefaultModelVertexFormats; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +public class SFPModelVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements ModelVertexSink { + public SFPModelVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, DefaultModelVertexFormats.MODEL_VERTEX_SFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + long i = this.writePointer; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + CompatMemoryUtil.memPutFloat(i + 16, u); + CompatMemoryUtil.memPutFloat(i + 20, v); + CompatMemoryUtil.memPutInt(i + 24, encodeLightMapTexCoord(light)); + + this.advance(); + } + + /** + * This moves some work out the shader code and simplifies things a bit. In vanilla, the game encodes light map + * texture coordinates as two un-normalized unsigned shorts in the range 0..255. Using the fixed-function pipeline, + * it then applies a matrix transformation which normalizes these coordinates and applies a centering offset. This + * operation has non-zero overhead and complicates shader code a bit. + * + * To work around the problem, this function instead normalizes these light map texture coordinates and applies the + * centering offset, allowing it to be baked into the vertex data itself. + * + * @param light The light map value + * @return The light map texture coordinates as two unsigned shorts with a center offset applied + */ + private static int encodeLightMapTexCoord(int light) { + int sl = (light >> 16) & 255; + sl = (sl << 8) + 2048; + + int bl = light & 255; + bl = (bl << 8) + 2048; + + return (sl << 16) | bl; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexType.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexType.java new file mode 100644 index 000000000..388a2c8a3 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/format/sfp/SFPModelVertexType.java @@ -0,0 +1,57 @@ +package me.jellysquid.mods.sodium.client.render.chunk.format.sfp; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; + +/** + * Simple vertex format which uses single-precision floating point numbers to represent position and texture + * coordinates. + */ +public class SFPModelVertexType implements ChunkVertexType { + public static final GlVertexFormat VERTEX_FORMAT = + GlVertexFormat.builder(ChunkMeshAttribute.class, 32) + .addElement(ChunkMeshAttribute.POSITION, 0, GlVertexAttributeFormat.FLOAT, 3, false) + .addElement(ChunkMeshAttribute.COLOR, 12, GlVertexAttributeFormat.UNSIGNED_BYTE, 4, true) + .addElement(ChunkMeshAttribute.TEXTURE, 16, GlVertexAttributeFormat.FLOAT, 2, false) + .addElement(ChunkMeshAttribute.LIGHT, 24, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, true) + .build(); + + public static final float MODEL_SCALE = 1.0f; + public static final float TEXTURE_SCALE = 1.0f; + + @Override + public ModelVertexSink createFallbackWriter(VertexConsumer consumer) { + throw new UnsupportedOperationException(); + } + + @Override + public ModelVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new SFPModelVertexBufferWriterUnsafe(buffer) : new SFPModelVertexBufferWriterNio(buffer); + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } + + @Override + public GlVertexFormat getCustomVertexFormat() { + return VERTEX_FORMAT; + } + + @Override + public float getModelScale() { + return MODEL_SCALE; + } + + @Override + public float getTextureScale() { + return TEXTURE_SCALE; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderList.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderList.java new file mode 100644 index 000000000..bfdaa6f80 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderList.java @@ -0,0 +1,118 @@ +package me.jellysquid.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.Arrays; + +/** + * A simple extension over {@link ObjectArrayList} which provides iterator methods in either FIFO or LIFO ordering. + */ +public class ChunkRenderList { + private T[] stateArray; + private int[] cullArray; + private int size, capacity; + + public ChunkRenderList() { + this(1024); + } + + @SuppressWarnings("unchecked") + public ChunkRenderList(int capacity) { + this.size = 0; + this.capacity = capacity; + + this.stateArray = (T[]) new Object[capacity]; + this.cullArray = new int[capacity]; + } + + private void resize() { + this.capacity = this.capacity * 2; + + this.stateArray = Arrays.copyOf(this.stateArray, this.capacity); + this.cullArray = Arrays.copyOf(this.cullArray, this.capacity); + } + + public void add(T state, int cull) { + int idx = this.size++; + + if (idx >= this.capacity) { + this.resize(); + } + + this.stateArray[idx] = state; + this.cullArray[idx] = cull; + } + + public void reset() { + if (this.size == 0) { + return; + } + + for (int i = 0; i < this.size; i++) { + this.stateArray[i] = null; + } + + for (int i = 0; i < this.size; i++) { + this.cullArray[i] = 0; + } + + this.size = 0; + } + + /** + * @return An iterator which returns elements in FIFO order or LIFO order if {@param backwards} is set + */ + public ChunkRenderListIterator iterator(boolean backwards) { + if (backwards) { + return new ChunkRenderListIterator() { + private int pos = ChunkRenderList.this.size - 1; + + @Override + public T getGraphicsState() { + return ChunkRenderList.this.stateArray[this.pos]; + } + + @Override + public int getVisibleFaces() { + return ChunkRenderList.this.cullArray[this.pos]; + } + + @Override + public boolean hasNext() { + return this.pos >= 0; + } + + @Override + public void advance() { + this.pos--; + } + }; + } else { + return new ChunkRenderListIterator() { + private final int lim = ChunkRenderList.this.size; + + private int pos = 0; + + @Override + public T getGraphicsState() { + return ChunkRenderList.this.stateArray[this.pos]; + } + + @Override + public int getVisibleFaces() { + return ChunkRenderList.this.cullArray[this.pos]; + } + + @Override + public boolean hasNext() { + return this.pos < this.lim; + } + + @Override + public void advance() { + this.pos++; + } + }; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderListIterator.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderListIterator.java new file mode 100644 index 000000000..9dc21f066 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/lists/ChunkRenderListIterator.java @@ -0,0 +1,9 @@ +package me.jellysquid.mods.sodium.client.render.chunk.lists; + +public interface ChunkRenderListIterator { + T getGraphicsState(); + int getVisibleFaces(); + + boolean hasNext(); + void advance(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/passes/BlockRenderPass.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/passes/BlockRenderPass.java new file mode 100644 index 000000000..ca92580ed --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/passes/BlockRenderPass.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.render.chunk.passes; + + +/** + * The order of these enums must match the passes used by vanilla. + */ +public enum BlockRenderPass { + CUTOUT_MIPPED(false), + TRANSLUCENT(true); + + public static final BlockRenderPass[] VALUES = BlockRenderPass.values(); + public static final int COUNT = VALUES.length; + + private final boolean translucent; + + BlockRenderPass(boolean translucent) { + this.translucent = translucent; + } + + public boolean isTranslucent() { + return this.translucent; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegion.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegion.java new file mode 100644 index 000000000..4be360a93 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegion.java @@ -0,0 +1,90 @@ +package me.jellysquid.mods.sodium.client.render.chunk.region; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import me.jellysquid.mods.sodium.client.gl.arena.GlBufferArena; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.tessellation.GlTessellation; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw.ChunkDrawCallBatcher; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; + +public class ChunkRegion { + private static final int EXPECTED_CHUNK_SIZE = 4 * 1024; + + private final GlBufferArena arena; + private final ChunkDrawCallBatcher batch; + private final RenderDevice device; + + private final ObjectArrayList> uploadQueue; + + private GlTessellation tessellation; + + private final int x, y, z; + + public float camDistance; + + public ChunkRegion(RenderDevice device, int size, int x, int y, int z) { + int arenaSize = EXPECTED_CHUNK_SIZE * size; + + this.device = device; + this.arena = new GlBufferArena(device, arenaSize, arenaSize); + this.uploadQueue = new ObjectArrayList<>(); + + this.batch = ChunkDrawCallBatcher.create(size * ModelQuadFacing.COUNT); + + this.x = x; + this.y = y; + this.z = z; + } + + public int getCenterBlockX() { + return (this.x * ChunkRegionManager.BUFFER_WIDTH * 16) + (ChunkRegionManager.BUFFER_WIDTH / 2 * 16); + } + + public int getCenterBlockY() { + return (this.y * ChunkRegionManager.BUFFER_HEIGHT * 16) + (ChunkRegionManager.BUFFER_HEIGHT / 2 * 16); + } + + public int getCenterBlockZ() { + return (this.z * ChunkRegionManager.BUFFER_LENGTH * 16) + (ChunkRegionManager.BUFFER_LENGTH / 2 * 16); + } + + public GlBufferArena getBufferArena() { + return this.arena; + } + + public boolean isArenaEmpty() { + return this.arena.isEmpty(); + } + + public void deleteResources() { + if (this.tessellation != null) { + try (CommandList commands = this.device.createCommandList()) { + commands.deleteTessellation(this.tessellation); + } + + this.tessellation = null; + } + + this.arena.delete(); + this.batch.delete(); + } + + public ObjectArrayList> getUploadQueue() { + return this.uploadQueue; + } + + public ChunkDrawCallBatcher getDrawBatcher() { + return this.batch; + } + + public GlTessellation getTessellation() { + return this.tessellation; + } + + public void setTessellation(GlTessellation tessellation) { + this.tessellation = tessellation; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegionManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegionManager.java new file mode 100644 index 000000000..f24860fd0 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/ChunkRegionManager.java @@ -0,0 +1,78 @@ +package me.jellysquid.mods.sodium.client.render.chunk.region; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.util.MathUtil; +import org.apache.commons.lang3.Validate; + +public class ChunkRegionManager { + public static final int BUFFER_WIDTH = 8; + public static final int BUFFER_HEIGHT = 4; + public static final int BUFFER_LENGTH = 8; + + public static final int BUFFER_SIZE = BUFFER_WIDTH * BUFFER_HEIGHT * BUFFER_LENGTH; + + private static final int BUFFER_WIDTH_SH = Integer.bitCount(BUFFER_WIDTH - 1); + private static final int BUFFER_HEIGHT_SH = Integer.bitCount(BUFFER_HEIGHT - 1); + private static final int BUFFER_LENGTH_SH = Integer.bitCount(BUFFER_LENGTH - 1); + + static { + Validate.isTrue(MathUtil.isPowerOfTwo(BUFFER_WIDTH)); + Validate.isTrue(MathUtil.isPowerOfTwo(BUFFER_LENGTH)); + Validate.isTrue(MathUtil.isPowerOfTwo(BUFFER_HEIGHT)); + } + + private final Long2ReferenceOpenHashMap> regions = new Long2ReferenceOpenHashMap<>(); + private final RenderDevice device; + + public ChunkRegionManager(RenderDevice device) { + this.device = device; + } + + public ChunkRegion getRegion(int x, int y, int z) { + return this.regions.get(getRegionKey(x, y, z)); + } + + public ChunkRegion getOrCreateRegion(int x, int y, int z) { + long key = getRegionKey(x, y, z); + + ChunkRegion region = this.regions.get(key); + + if (region == null) { + this.regions.put(key, region = new ChunkRegion<>(this.device, BUFFER_SIZE, x >> BUFFER_WIDTH_SH, y >> BUFFER_HEIGHT_SH, z >> BUFFER_LENGTH_SH)); + } + + return region; + } + + public static long getRegionKey(int x, int y, int z) { + return ChunkSectionPos.asLong(x >> BUFFER_WIDTH_SH, y >> BUFFER_HEIGHT_SH, z >> BUFFER_LENGTH_SH); + } + + public void delete() { + for (ChunkRegion region : this.regions.values()) { + region.deleteResources(); + } + + this.regions.clear(); + } + + public void cleanup() { + for (ObjectIterator> iterator = this.regions.values().iterator(); iterator.hasNext(); ) { + ChunkRegion region = iterator.next(); + + if (region.isArenaEmpty()) { + region.deleteResources(); + + iterator.remove(); + } + } + } + + public int getAllocatedRegionCount() { + return this.regions.size(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkFogMode.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkFogMode.java new file mode 100644 index 000000000..1e81e0444 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkFogMode.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.client.render.chunk.shader; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.function.Function; + +public enum ChunkFogMode { + NONE(ChunkShaderFogComponent.None::new, ImmutableList.of()), + LINEAR(ChunkShaderFogComponent.Linear::new, ImmutableList.of("USE_FOG", "USE_FOG_LINEAR")), + EXP2(ChunkShaderFogComponent.Exp2::new, ImmutableList.of("USE_FOG", "USE_FOG_EXP2")); + + private final Function factory; + private final List defines; + + ChunkFogMode(Function factory, List defines) { + this.factory = factory; + this.defines = defines; + } + + public Function getFactory() { + return this.factory; + } + + public List getDefines() { + return this.defines; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkProgram.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkProgram.java new file mode 100644 index 000000000..f2e05b687 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkProgram.java @@ -0,0 +1,52 @@ +package me.jellysquid.mods.sodium.client.render.chunk.shader; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.shader.GlProgram; +import me.jellysquid.mods.sodium.client.render.GameRendererContext; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.util.function.Function; + +/** + * A forward-rendering shader program for chunks. + */ +public class ChunkProgram extends GlProgram { + // Uniform variable binding indexes + private final int uModelViewProjectionMatrix; + private final int uModelScale; + private final int uTextureScale; + private final int uBlockTex; + private final int uLightTex; + + // The fog shader component used by this program in order to setup the appropriate GL state + private final ChunkShaderFogComponent fogShader; + + protected ChunkProgram(RenderDevice owner, ResourceLocation name, int handle, Function fogShaderFunction) { + super(owner, name, handle); + + this.uModelViewProjectionMatrix = this.getUniformLocation("u_ModelViewProjectionMatrix"); + + this.uBlockTex = this.getUniformLocation("u_BlockTex"); + this.uLightTex = this.getUniformLocation("u_LightTex"); + this.uModelScale = this.getUniformLocation("u_ModelScale"); + this.uTextureScale = this.getUniformLocation("u_TextureScale"); + + this.fogShader = fogShaderFunction.apply(this); + } + + public void setup(MatrixStack matrixStack, float modelScale, float textureScale) { + if(this.uBlockTex != -1) GL20.glUniform1i(this.uBlockTex, OpenGlHelper.defaultTexUnit - GL13.GL_TEXTURE0); + if(this.uLightTex != -1) GL20.glUniform1i(this.uLightTex, OpenGlHelper.lightmapTexUnit - GL13.GL_TEXTURE0); + + if(this.uModelScale != -1) GL20.glUniform3f(this.uModelScale, modelScale, modelScale, modelScale); + if(this.uTextureScale != -1) GL20.glUniform2f(this.uTextureScale, textureScale, textureScale); + + this.fogShader.setup(); + + if(this.uModelViewProjectionMatrix != -1) GL20.glUniformMatrix4(this.uModelViewProjectionMatrix, false, GameRendererContext.getModelViewProjectionMatrix(matrixStack.peek())); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java new file mode 100644 index 000000000..accd9ea15 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java @@ -0,0 +1,209 @@ +package me.jellysquid.mods.sodium.client.render.chunk.shader; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.gl.compat.FogHelper; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.shader.GlProgram; +import me.jellysquid.mods.sodium.client.gl.shader.GlShader; +import me.jellysquid.mods.sodium.client.gl.shader.ShaderConstants; +import me.jellysquid.mods.sodium.client.gl.shader.ShaderLoader; +import me.jellysquid.mods.sodium.client.gl.shader.ShaderType; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderBackend; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.pipeline.SodiumTerrainPipeline; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.shaderpack.transform.StringTransformations; +import net.coderbot.iris.shaderpack.transform.Transformations; +import net.coderbot.iris.shadows.ShadowRenderingState; +import net.coderbot.iris.sodium.shader_overrides.IrisChunkProgramOverrides; +import net.coderbot.iris.sodium.vertex_format.IrisModelVertexFormats; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.EnumMap; +import java.util.List; + +public abstract class ChunkRenderShaderBackend implements ChunkRenderBackend { + + private final EnumMap programs = new EnumMap<>(ChunkFogMode.class); + + protected final ChunkVertexType vertexType; + protected final GlVertexFormat vertexFormat; + + protected ChunkProgram activeProgram; + + // Iris + private IrisChunkProgramOverrides irisChunkProgramOverrides; + + private RenderDevice device; + + private ChunkProgram override; + + public ChunkRenderShaderBackend(ChunkVertexType vertexType) { + if (AngelicaConfig.enableIris) { + irisChunkProgramOverrides = new IrisChunkProgramOverrides(); + } + + this.vertexType = vertexType; + this.vertexFormat = vertexType.getCustomVertexFormat(); + } + + public GlShader loadShaderRedirect(RenderDevice device, ShaderType type, ResourceLocation name, List constants) { + if (AngelicaConfig.enableIris && this.vertexType == IrisModelVertexFormats.MODEL_VERTEX_XHFP) { + String shader = getShaderSource(ShaderLoader.getShaderPath(name, type)); + shader = shader.replace("v_LightCoord = a_LightCoord", "v_LightCoord = (iris_LightmapTextureMatrix * vec4(a_LightCoord, 0, 1)).xy"); + + StringTransformations transformations = new StringTransformations(shader); + + transformations.injectLine( + Transformations.InjectionPoint.BEFORE_CODE, + "mat4 iris_LightmapTextureMatrix = mat4(vec4(0.00390625, 0.0, 0.0, 0.0), vec4(0.0, 0.00390625, 0.0, 0.0), vec4(0.0, 0.0, 0.00390625, 0.0), vec4(0.03125, 0.03125, 0.03125, 1.0));"); + + return new GlShader(device, type, name, transformations.toString(), ShaderConstants.fromStringList(constants)); + } else { + return ShaderLoader.loadShader(device, type, name, constants); + } + } + + private ChunkProgram createShader(RenderDevice device, ChunkFogMode fogMode, GlVertexFormat vertexFormat) { + if(AngelicaConfig.enableIris) { + this.device = device; + WorldRenderingPipeline worldRenderingPipeline = Iris.getPipelineManager().getPipelineNullable(); + SodiumTerrainPipeline sodiumTerrainPipeline = null; + + if (worldRenderingPipeline != null) { + sodiumTerrainPipeline = worldRenderingPipeline.getSodiumTerrainPipeline(); + } + + irisChunkProgramOverrides.createShaders(sodiumTerrainPipeline, device); + } + + GlShader vertShader = loadShaderRedirect(device, ShaderType.VERTEX, new ResourceLocation("sodium", "chunk_gl20"), fogMode.getDefines()); + + GlShader fragShader = ShaderLoader.loadShader(device, ShaderType.FRAGMENT, new ResourceLocation("sodium", "chunk_gl20"), fogMode.getDefines()); + + try { + return GlProgram.builder(new ResourceLocation("sodium", "chunk_shader")) + .attachShader(vertShader) + .attachShader(fragShader) + .bindAttribute("a_Pos", ChunkShaderBindingPoints.POSITION) + .bindAttribute("a_Color", ChunkShaderBindingPoints.COLOR) + .bindAttribute("a_TexCoord", ChunkShaderBindingPoints.TEX_COORD) + .bindAttribute("a_LightCoord", ChunkShaderBindingPoints.LIGHT_COORD) + .bindAttribute("d_ModelOffset", ChunkShaderBindingPoints.MODEL_OFFSET) + .build((program, name) -> new ChunkProgram(device, program, name, fogMode.getFactory())); + } finally { + vertShader.delete(); + fragShader.delete(); + } + } + + + + private static String getShaderSource(String path) { + try { + InputStream in = ShaderLoader.class.getResourceAsStream(path); + Throwable throwable = null; + + String res; + try { + if (in == null) { + throw new RuntimeException("Shader not found: " + path); + } + + res = IOUtils.toString(in, StandardCharsets.UTF_8); + } catch (Throwable tr) { + throwable = tr; + throw tr; + } finally { + if (in != null) { + if (throwable != null) { + try { + in.close(); + } catch (Throwable var12) { + throwable.addSuppressed(var12); + } + } else { + in.close(); + } + } + + } + + return res; + } catch (IOException e) { + throw new RuntimeException("Could not read shader sources", e); + } + } + + + @Override + public final void createShaders(RenderDevice device) { + this.programs.put(ChunkFogMode.NONE, this.createShader(device, ChunkFogMode.NONE, this.vertexFormat)); + this.programs.put(ChunkFogMode.LINEAR, this.createShader(device, ChunkFogMode.LINEAR, this.vertexFormat)); + this.programs.put(ChunkFogMode.EXP2, this.createShader(device, ChunkFogMode.EXP2, this.vertexFormat)); + } + + @Override + public void begin(MatrixStack matrixStack) { + this.activeProgram = this.programs.get(FogHelper.getFogMode()); + if (AngelicaConfig.enableIris && override != null) { + this.activeProgram = override; + } + this.activeProgram.bind(); + this.activeProgram.setup(matrixStack, this.vertexType.getModelScale(), this.vertexType.getTextureScale()); + } + + @Override + public void end(MatrixStack matrixStack) { + this.activeProgram.unbind(); + this.activeProgram = null; + if(AngelicaConfig.enableIris) { + ProgramUniforms.clearActiveUniforms(); + ProgramSamplers.clearActiveSamplers(); + Iris.getPipelineManager().getPipeline().ifPresent(WorldRenderingPipeline::endSodiumTerrainRendering); + } + } + + @Override + public void delete() { + if(AngelicaConfig.enableIris) { + irisChunkProgramOverrides.deleteShaders(); + } + for (ChunkProgram shader : this.programs.values()) { + shader.delete(); + } + } + + @Override + public ChunkVertexType getVertexType() { + return this.vertexType; + } + + // Iris + public void iris$begin(MatrixStack matrixStack, BlockRenderPass pass) { + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + // No back face culling during the shadow pass + // TODO: Hopefully this won't be necessary in the future... + GLStateManager.disableCull(); + } + + this.override = irisChunkProgramOverrides.getProgramOverride(device, pass); + + Iris.getPipelineManager().getPipeline().ifPresent(WorldRenderingPipeline::beginSodiumTerrainRendering); + begin(matrixStack); + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderBindingPoints.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderBindingPoints.java new file mode 100644 index 000000000..9b1b6d3d6 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderBindingPoints.java @@ -0,0 +1,12 @@ +package me.jellysquid.mods.sodium.client.render.chunk.shader; + +import me.jellysquid.mods.sodium.client.gl.shader.ShaderBindingPoint; + +public class ChunkShaderBindingPoints { + public static final ShaderBindingPoint POSITION = new ShaderBindingPoint(0); + public static final ShaderBindingPoint COLOR = new ShaderBindingPoint(1); + public static final ShaderBindingPoint TEX_COORD = new ShaderBindingPoint(2); + public static final ShaderBindingPoint LIGHT_COORD = new ShaderBindingPoint(3); + + public static final ShaderBindingPoint MODEL_OFFSET = new ShaderBindingPoint(4); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java new file mode 100644 index 000000000..f32da240e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java @@ -0,0 +1,70 @@ +package me.jellysquid.mods.sodium.client.render.chunk.shader; + +import me.jellysquid.mods.sodium.client.gl.compat.FogHelper; +import org.lwjgl.opengl.GL20; + +/** + * These shader implementations try to remain compatible with the deprecated fixed function pipeline by manually + * copying the state into each shader's uniforms. The shader code itself is a straight-forward implementation of the + * fog functions themselves from the fixed-function pipeline, except that they use the distance from the camera + * rather than the z-buffer to produce better looking fog that doesn't move with the player's view angle. + * + * Minecraft itself will actually try to enable distance-based fog by using the proprietary NV_fog_distance extension, + * but as the name implies, this only works on graphics cards produced by NVIDIA. The shader implementation however does + * not depend on any vendor-specific extensions and is written using very simple GLSL code. + */ +public abstract class ChunkShaderFogComponent { + public abstract void setup(); + + public static class None extends ChunkShaderFogComponent { + public None(ChunkProgram program) { + + } + + @Override + public void setup() { + + } + } + + public static class Exp2 extends ChunkShaderFogComponent { + private final int uFogColor; + private final int uFogDensity; + + public Exp2(ChunkProgram program) { + this.uFogColor = program.getUniformLocation("u_FogColor"); + this.uFogDensity = program.getUniformLocation("u_FogDensity"); + } + + @Override + public void setup() { + final float[] fc = FogHelper.getFogColor(); + GL20.glUniform4f(this.uFogColor, fc[0], fc[1], fc[2], fc[3]); + GL20.glUniform1f(this.uFogDensity, FogHelper.getFogDensity()); + } + } + + public static class Linear extends ChunkShaderFogComponent { + private final int uFogColor; + private final int uFogLength; + private final int uFogEnd; + + public Linear(ChunkProgram program) { + this.uFogColor = program.getUniformLocation("u_FogColor"); + this.uFogLength = program.getUniformLocation("u_FogLength"); + this.uFogEnd = program.getUniformLocation("u_FogEnd"); + } + + @Override + public void setup() { + float end = FogHelper.getFogEnd(); + float start = FogHelper.getFogStart(); + float[] color = FogHelper.getFogColor(); + + GL20.glUniform4f(this.uFogColor, color[0], color[1], color[2], color[3]); + GL20.glUniform1f(this.uFogLength, end - start); + GL20.glUniform1f(this.uFogEnd, end); + } + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderBuildTask.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderBuildTask.java new file mode 100644 index 000000000..3bd799cdb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderBuildTask.java @@ -0,0 +1,40 @@ +package me.jellysquid.mods.sodium.client.render.chunk.tasks; + +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheLocal; +import me.jellysquid.mods.sodium.client.util.task.CancellationSource; + +/** + * Build tasks are immutable jobs (with optional prioritization) which contain all the necessary state to perform + * chunk mesh updates or quad sorting off the main thread. + * + * When a task is constructed on the main thread, it should copy all the state it requires in order to complete the task + * without further synchronization. The task will then be scheduled for async execution on a thread pool. + * + * After the task completes, it returns a "build result" which contains any computed data that needs to be handled + * on the main thread. + * + * @param The graphics state of the chunk render + */ +public abstract class ChunkRenderBuildTask { + /** + * Executes the given build task asynchronously from the calling thread. The implementation should be careful not + * to access or modify global mutable state. + * + * @param cache The render cache to use for building this chunk + * @param buffers The temporary scratch buffers for rendering block data + * @param cancellationSource The cancellation source which can be used to query if the task is cancelled + * @return The build result of this task, containing any data which needs to be uploaded on the main-thread, or null + * if the task was cancelled. + */ + public abstract ChunkBuildResult performBuild(ChunkRenderCacheLocal cache, ChunkBuildBuffers buffers, + CancellationSource cancellationSource); + + /** + * Called on the main render thread when the task's execution returns. The implementation should release any + * resources it's still holding onto at this point. + */ + public abstract void releaseResources(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderEmptyBuildTask.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderEmptyBuildTask.java new file mode 100644 index 000000000..b91532f62 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderEmptyBuildTask.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.render.chunk.tasks; + +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheLocal; +import me.jellysquid.mods.sodium.client.util.task.CancellationSource; + +/** + * A build task which does no computation and always return an empty build result. These tasks are created whenever + * chunk meshes need to be deleted as the only way to change graphics state is to send a message to the main + * actor thread. In cases where new chunk renders are being created and scheduled, the scheduler will prefer to just + * synchronously update the render's data to an empty state to speed things along. + */ +public class ChunkRenderEmptyBuildTask extends ChunkRenderBuildTask { + private final ChunkRenderContainer render; + + public ChunkRenderEmptyBuildTask(ChunkRenderContainer render) { + this.render = render; + } + + @Override + public ChunkBuildResult performBuild(ChunkRenderCacheLocal cache, ChunkBuildBuffers buffers, CancellationSource cancellationSource) { + return new ChunkBuildResult<>(this.render, ChunkRenderData.EMPTY); + } + + @Override + public void releaseResources() { + + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java new file mode 100644 index 000000000..b12e5b60f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java @@ -0,0 +1,277 @@ +package me.jellysquid.mods.sodium.client.render.chunk.tasks; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionDataBuilder; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.TessellatorManager; +import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry; +import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderBounds; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheLocal; +import me.jellysquid.mods.sodium.client.util.MathUtil; +import me.jellysquid.mods.sodium.client.util.task.CancellationSource; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import me.jellysquid.mods.sodium.client.world.cloned.ChunkRenderContext; +import me.jellysquid.mods.sodium.common.config.SodiumConfig; +import net.coderbot.iris.vertices.ExtendedDataHelper; +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.AxisAlignedBB; +import net.minecraftforge.fluids.IFluidBlock; +import org.joml.Vector3d; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * Rebuilds all the meshes of a chunk for each given render pass with non-occluded blocks. The result is then uploaded + * to graphics memory on the main thread. + * + * This task takes a slice of the world from the thread it is created on. Since these slices require rather large + * array allocations, they are pooled to ensure that the garbage collector doesn't become overloaded. + */ +public class ChunkRenderRebuildTask extends ChunkRenderBuildTask { + private final ChunkRenderContainer render; + + private final BlockPos offset; + + private final ChunkRenderContext context; + + private Vector3d camera; + + private final boolean translucencySorting; + + public ChunkRenderRebuildTask(ChunkRenderContainer render, ChunkRenderContext context, BlockPos offset) { + this.render = render; + this.offset = offset; + this.context = context; + this.camera = new Vector3d(); + this.translucencySorting = SodiumClientMod.options().advanced.translucencySorting; + + } + + public ChunkRenderRebuildTask withCameraPosition(Vector3d camera) { + this.camera = camera; + return this; + } + + private boolean rendersOutsideBoundingBox(TileEntity entity, int baseX, int baseY, int baseZ) { + AxisAlignedBB box = entity.getRenderBoundingBox(); + + // Check if it's explictly infinite + if(box == TileEntity.INFINITE_EXTENT_AABB) + return true; + + // Check if it extends outside our minimums + if(box.minX < baseX || box.minY < baseY || box.minZ < baseZ) + return true; + + // Check if it extends outside our maximums + if(box.maxX > (baseX + 16) || box.maxY > (baseY + 16) || box.maxZ > (baseZ + 16)) + return true; + + // So it's within the chunk + return false; + } + + private boolean rendersOffThread(Block block) { + int type = block.getRenderType(); + return type < 42 && type != 22 && AngelicaBlockSafetyRegistry.canBlockRenderOffThread(block); + } + + @Override + public ChunkBuildResult performBuild(ChunkRenderCacheLocal cache, ChunkBuildBuffers buffers, CancellationSource cancellationSource) { + // COMPATIBLITY NOTE: Oculus relies on the LVT of this method being unchanged, at least in 16.5 + ChunkRenderData.Builder renderData = new ChunkRenderData.Builder(); + ChunkOcclusionDataBuilder occluder = new ChunkOcclusionDataBuilder(); + ChunkRenderBounds.Builder bounds = new ChunkRenderBounds.Builder(); + + buffers.init(renderData); + + cache.init(this.context); + + final WorldSlice slice = cache.getWorldSlice(); + final RenderBlocks renderBlocks = new RenderBlocks(slice); + + int baseX = this.render.getOriginX(); + int baseY = this.render.getOriginY(); + int baseZ = this.render.getOriginZ(); + + BlockPos.Mutable pos = new BlockPos.Mutable(); + BlockPos renderOffset = this.offset; + final Tessellator tessellator = TessellatorManager.get(); + + boolean hasMainThreadBlocks = false; + + for (int relY = 0; relY < 16; relY++) { + if (cancellationSource.isCancelled()) { + return null; + } + + for (int relZ = 0; relZ < 16; relZ++) { + for (int relX = 0; relX < 16; relX++) { + Block block = slice.getBlockRelative(relX + 16, relY + 16, relZ + 16); + + // If the block is vanilla air, assume it renders nothing. Don't use isAir because mods + // can abuse it for all sorts of things + if (block.getMaterial() == Material.air) { + continue; + } + + int meta = slice.getBlockMetadataRelative(relX + 16, relY + 16, relZ + 16); + + pos.set(baseX + relX, baseY + relY, baseZ + relZ); + buffers.setRenderOffset(pos.x - renderOffset.getX(), pos.y - renderOffset.getY(), pos.z - renderOffset.getZ()); + + if(AngelicaConfig.enableIris) buffers.iris$setLocalPos(relX, relY, relZ); + + if (rendersOffThread(block)) { + // Do regular block rendering + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + if (block.canRenderInPass(pass.ordinal()) && (!AngelicaConfig.enableSodiumFluidRendering || !(block instanceof IFluidBlock))) { + long seed = MathUtil.hashPos(pos.x, pos.y, pos.z); + if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); + + if (cache.getBlockRenderer().renderModel(cache.getWorldSlice(), tessellator, renderBlocks, block, meta, pos, buffers.get(pass), true, seed)) { + bounds.addBlock(relX, relY, relZ); + } + } + } + } else { + hasMainThreadBlocks = true; + } + + // Do fluid rendering without RenderBlocks + if (AngelicaConfig.enableSodiumFluidRendering && block instanceof IFluidBlock) { + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + if (block.canRenderInPass(pass.ordinal())) { + if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.FLUID_RENDER_TYPE); + + if (cache.getFluidRenderer().render(slice, cache.getWorldSlice(), block, pos, buffers.get(pass))) { + bounds.addBlock(relX, relY, relZ); + } + } + } + } + + if(AngelicaConfig.enableIris) buffers.iris$resetBlockContext(); + + if (block.hasTileEntity(meta)) { + TileEntity entity = slice.getTileEntity(pos.x, pos.y, pos.z); + + final TileEntitySpecialRenderer renderer = TileEntityRendererDispatcher.instance.getSpecialRenderer(entity); + if (entity != null && renderer != null) { + renderData.addTileEntity(entity, !rendersOutsideBoundingBox(entity, baseX, baseY, baseZ)); + bounds.addBlock(relX, relY, relZ); + } + } + + if (block.isOpaqueCube()) { + occluder.markClosed(pos); + } + } + } + } + + if(hasMainThreadBlocks) { + // Render the other blocks on the main thread + try { + CompletableFuture.runAsync(() -> this.performMainBuild(cache, buffers, cancellationSource, bounds), AngelicaRenderQueue.executor()).get(); + } catch(InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch(ExecutionException e) { + throw new RuntimeException(e); + } + // Check if cancellation happened during that, so we don't render an incomplete chunk + if(cancellationSource.isCancelled()) return null; + } + + render.setRebuildForTranslucents(false); + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + ChunkMeshData mesh = buffers.createMesh(pass, (float)camera.x - offset.getX(), (float)camera.y - offset.getY(), (float)camera.z - offset.getZ(), this.translucencySorting); + + if (mesh != null) { + renderData.setMesh(pass, mesh); + if(this.translucencySorting && pass.isTranslucent()) + render.setRebuildForTranslucents(true); + } + } + + renderData.setOcclusionData(occluder.build()); + renderData.setBounds(bounds.build(this.render.getChunkPos())); + + return new ChunkBuildResult<>(this.render, renderData.build()); + } + + /** + * Render the blocks that should be rendered on the main thread. + * + * TODO: Deduplicate this with the main method above. + */ + private void performMainBuild(ChunkRenderCacheLocal cache, ChunkBuildBuffers buffers, CancellationSource cancellationSource, ChunkRenderBounds.Builder bounds) { + WorldSlice slice = cache.getWorldSlice(); + BlockPos.Mutable pos = new BlockPos.Mutable(); + int baseX = this.render.getOriginX(); + int baseY = this.render.getOriginY(); + int baseZ = this.render.getOriginZ(); + BlockPos renderOffset = this.offset; + RenderBlocks rb = new RenderBlocks(slice); + for (int relY = 0; relY < 16; relY++) { + if (cancellationSource.isCancelled()) { + return; + } + for (int relZ = 0; relZ < 16; relZ++) { + for (int relX = 0; relX < 16; relX++) { + Block block = slice.getBlockRelative(relX + 16, relY + 16, relZ + 16); + + // Only render blocks that need main thread assistance + if (block.getMaterial() == Material.air || rendersOffThread(block)) { + continue; + } + + // TODO: Collect data on which render types are hitting this code path most often + // so mods can be updated slowly + + int meta = slice.getBlockMetadataRelative(relX + 16, relY + 16, relZ + 16); + + pos.set(baseX + relX, baseY + relY, baseZ + relZ); + buffers.setRenderOffset(pos.x - renderOffset.getX(), pos.y - renderOffset.getY(), pos.z - renderOffset.getZ()); + if(AngelicaConfig.enableIris) buffers.iris$setLocalPos(relX, relY, relZ); + + // Do regular block rendering + for (BlockRenderPass pass : BlockRenderPass.VALUES) { + if (block.canRenderInPass(pass.ordinal()) && (!AngelicaConfig.enableSodiumFluidRendering || !(block instanceof IFluidBlock))) { + long seed = MathUtil.hashPos(pos.x, pos.y, pos.z); + if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); + + if (cache.getBlockRenderer().renderModel(cache.getWorldSlice(), Tessellator.instance, rb, block, meta, pos, buffers.get(pass), true, seed)) { + bounds.addBlock(relX, relY, relZ); + } + } + } + + if(AngelicaConfig.enableIris) buffers.iris$resetBlockContext(); + } + } + } + } + + @Override + public void releaseResources() { + this.context.releaseResources(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderTranslucencySortTask.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderTranslucencySortTask.java new file mode 100644 index 000000000..69a0cdc6e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderTranslucencySortTask.java @@ -0,0 +1,92 @@ +package me.jellysquid.mods.sodium.client.render.chunk.tasks; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import me.jellysquid.mods.sodium.client.gl.buffer.VertexData; +import me.jellysquid.mods.sodium.client.gl.util.BufferSlice; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; +import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBufferSorter; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.compile.ChunkBuildResult; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkMeshData; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.pipeline.context.ChunkRenderCacheLocal; +import me.jellysquid.mods.sodium.client.util.task.CancellationSource; +import org.joml.Vector3d; +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Handles sorting translucency data in built chunks. + */ +public class ChunkRenderTranslucencySortTask extends ChunkRenderBuildTask { + private static final BlockRenderPass[] TRANSLUCENT_PASSES = Arrays.stream(BlockRenderPass.VALUES).filter(BlockRenderPass::isTranslucent).toArray(BlockRenderPass[]::new); + + private final ChunkRenderContainer render; + private final BlockPos offset; + private final Vector3d camera; + + public ChunkRenderTranslucencySortTask(ChunkRenderContainer render, BlockPos offset, Vector3d camera) { + this.render = render; + this.offset = offset; + this.camera = camera; + + } + + + @Override + public ChunkBuildResult performBuild(ChunkRenderCacheLocal cache, ChunkBuildBuffers buffers, CancellationSource cancellationSource) { + ChunkRenderData data = this.render.getData(); + if(data.isEmpty()) + return null; + + Map replacementMeshes = new HashMap<>(); + for(BlockRenderPass pass : TRANSLUCENT_PASSES) { + ChunkGraphicsState state = this.render.getGraphicsState(pass); + if(state == null) + continue; + ByteBuffer translucencyData = state.getTranslucencyData(); + if(translucencyData == null) + continue; + ChunkMeshData translucentMesh = data.getMesh(pass); + if(translucentMesh == null) + continue; + + // Make a snapshot of the translucency data to sort + ByteBuffer sortedData = BufferUtils.createByteBuffer(translucencyData.capacity()); + synchronized (translucencyData) { + sortedData.put(translucencyData); + translucencyData.position(0); + translucencyData.limit(translucencyData.capacity()); + } + + sortedData.flip(); + // Sort it and create the new mesh + ChunkBufferSorter.sortStandardFormat(buffers.getVertexType(), sortedData, sortedData.capacity(), (float) camera.x - offset.getX(), (float)camera.y - offset.getY(), (float)camera.z - offset.getZ()); + ChunkMeshData newMesh = new ChunkMeshData(); + newMesh.setVertexData(new VertexData(sortedData, buffers.getVertexType().getCustomVertexFormat())); + for(Map.Entry entry : translucentMesh.getSlices()) { + newMesh.setModelSlice(entry.getKey(), entry.getValue()); + } + replacementMeshes.put(pass, newMesh); + } + + if(replacementMeshes.isEmpty()) + return null; + + ChunkBuildResult result = new ChunkBuildResult<>(this.render, data.copyAndReplaceMesh(replacementMeshes)); + result.passesToUpload = replacementMeshes.keySet().toArray(new BlockRenderPass[0]); + return result; + } + + @Override + public void releaseResources() { + + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/entity/EntityLightSampler.java b/src/main/java/me/jellysquid/mods/sodium/client/render/entity/EntityLightSampler.java new file mode 100644 index 000000000..afad9c830 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/entity/EntityLightSampler.java @@ -0,0 +1,10 @@ +package me.jellysquid.mods.sodium.client.render.entity; + +import net.minecraft.entity.Entity; +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; + +public interface EntityLightSampler { + int bridge$getBlockLight(T entity, BlockPos pos); + + int bridge$getSkyLight(T entity, BlockPos pos); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java new file mode 100644 index 000000000..39bb9e67d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java @@ -0,0 +1,250 @@ +package me.jellysquid.mods.sodium.client.render.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.compat.nd.Quad; +import com.gtnewhorizons.angelica.compat.nd.RecyclingList; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.mixins.interfaces.ITessellatorInstance; +import me.jellysquid.mods.sodium.client.model.light.LightMode; +import me.jellysquid.mods.sodium.client.model.light.LightPipeline; +import me.jellysquid.mods.sodium.client.model.light.LightPipelineProvider; +import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadOrientation; +import me.jellysquid.mods.sodium.client.render.chunk.compile.buffers.ChunkModelBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.data.ChunkRenderData; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.util.ModelQuadUtil; +import me.jellysquid.mods.sodium.client.util.rand.XoRoShiRoRandom; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3d; +import org.lwjgl.opengl.GL11; + +import java.util.List; +import java.util.Random; + +public class BlockRenderer { + + private final Random random = new XoRoShiRoRandom(); + + private final QuadLightData cachedQuadLightData = new QuadLightData(); + + private final boolean useAmbientOcclusion; + private boolean useSeparateAo; + + private final LightPipelineProvider lighters; + + private final Flags FLAGS = new Flags(true, true, true, false); + private final RecyclingList quadBuf = new RecyclingList<>(Quad::new); + + + public BlockRenderer(LightPipelineProvider lighters) { + this.lighters = lighters; + // TODO: Sodium - AO Setting + this.useAmbientOcclusion = Minecraft.getMinecraft().gameSettings.ambientOcclusion > 0; + } + + public boolean renderModel(IBlockAccess world, Tessellator tessellator, RenderBlocks renderBlocks, Block block, int meta, BlockPos pos, ChunkModelBuffers buffers, boolean cull, long seed) { + final LightMode mode = this.getLightingMode(block); + final LightPipeline lighter = this.lighters.getLighter(mode); + + this.useSeparateAo = AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); + + boolean rendered = false; + + try { + tessellator.startDrawingQuads(); + renderBlocks.renderBlockByRenderType(block, pos.x, pos.y, pos.z); + + + final List all = tessellatorToQuadList(tessellator, pos); + + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + this.random.setSeed(seed); + this.renderQuadList(pos, lighter, buffers, all, facing); + } + + if (!all.isEmpty()) rendered = true; + } finally { + ((ITessellatorInstance) tessellator).discard(); + } + + + return rendered; + } + + private int tesselatorDataCount; + private List tessellatorToQuadList(Tessellator t, BlockPos pos) { + // Adapted from Neodymium + tesselatorDataCount++; + +// List errors = new ArrayList<>(); +// List warnings = new ArrayList<>(); +// if(t.drawMode != GL11.GL_QUADS && t.drawMode != GL11.GL_TRIANGLES) { +// errors.add("Unsupported draw mode: " + t.drawMode); +// } +// if(!t.hasTexture) { +// errors.add("Texture data is missing."); +// } +// if(!t.hasBrightness) { +// warnings.add("Brightness data is missing"); +// } +// if(!t.hasColor) { +// warnings.add("Color data is missing"); +// } +// if(t.hasNormals && GL11.glIsEnabled(GL11.GL_LIGHTING)) { +// errors.add("Chunk uses GL lighting, this is not implemented."); +// } + FLAGS.hasBrightness = t.hasBrightness; + FLAGS.hasColor = t.hasColor; + + int verticesPerPrimitive = t.drawMode == GL11.GL_QUADS ? 4 : 3; + + for(int quadI = 0; quadI < t.vertexCount / verticesPerPrimitive; quadI++) { + final Quad quad = quadBuf.next(); + // RenderBlocks adds the subchunk-relative coordinates as the offset, cancel it out here + quad.setState(t.rawBuffer, quadI * (verticesPerPrimitive * 8), FLAGS, t.drawMode, -pos.x, -pos.y, -pos.z); + + if(quad.deleted) { + quadBuf.remove(); + } + } +// final boolean silenceErrors = false; +// +// if(!quadBuf.isEmpty() && (!errors.isEmpty() || !warnings.isEmpty()) && /*!Config.silenceErrors*/!silenceErrors) { +// for(String error : errors) { +// LOGGER.error("Error: " + error); +// } +// for(String warning : warnings) { +// LOGGER.error("Warning: " + warning); +// } +// LOGGER.error("(Tessellator pos: ({}, {}, {}), Tessellation count: {}", t.xOffset, t.yOffset, t.zOffset, tesselatorDataCount); +// LOGGER.error("Stack trace:"); +// try { +// // Generate a stack trace +// throw new IllegalArgumentException(); +// } catch(IllegalArgumentException e) { +// e.printStackTrace(); +// } +// } + final List quads = quadBuf.getAsList(); + quadBuf.reset(); + return quads; + } + + private void renderQuadList(BlockPos pos, LightPipeline lighter, ChunkModelBuffers buffers, List quads, ModelQuadFacing facing) { + + final ModelVertexSink sink = buffers.getSink(facing); + sink.ensureCapacity(quads.size() * 4); + final ForgeDirection cullFace = ModelQuadFacing.toDirection(facing); + + final ChunkRenderData.Builder renderData = buffers.getRenderData(); + + // This is a very hot allocation, iterate over it manually + // noinspection ForLoopReplaceableByForEach + for (int i = 0, quadsSize = quads.size(); i < quadsSize; i++) { + final Quad quad = quads.get(i); + + if(quad.normal != facing) + continue; + + final QuadLightData light = this.cachedQuadLightData; + + // TODO: use Sodium pipeline always + if(this.useSeparateAo) { + lighter.calculate(quad, pos, light, cullFace, quad.getFace(), quad.hasShade()); + } + + + this.renderQuad(sink, quad, light, renderData); + } + + sink.flush(); + } + + private void renderQuad(ModelVertexSink sink, Quad quad, QuadLightData light, ChunkRenderData.Builder renderData) { + + final ModelQuadOrientation order = useSeparateAo ? ModelQuadOrientation.orient(light.br) : ModelQuadOrientation.NORMAL; + + for (int dstIndex = 0; dstIndex < 4; dstIndex++) { + final int srcIndex = order.getVertexIndex(dstIndex); + + final float x = quad.getX(srcIndex); + final float y = quad.getY(srcIndex); + final float z = quad.getZ(srcIndex); + + int color = quad.getColor(srcIndex); + final float ao = light.br[srcIndex]; + if (useSeparateAo) { + color &= 0x00FFFFFF; + color |= ((int) (ao * 255.0f)) << 24; + } + + final float u = quad.getTexU(srcIndex); + final float v = quad.getTexV(srcIndex); + + int quadLight = quad.getLight(srcIndex); + final int lm = useSeparateAo ? ModelQuadUtil.mergeBakedLight(quadLight, light.lm[srcIndex]) : quadLight; + + sink.writeQuad(x, y, z, color, u, v, lm); + } + + final TextureAtlasSprite sprite = quad.rubidium$getSprite(); + + if (sprite != null) { + renderData.addSprite(sprite); + } + } + private LightMode getLightingMode(Block block) { + if (this.useAmbientOcclusion && block.getAmbientOcclusionLightValue() != 1.0F && block.getLightValue() == 0) { + return LightMode.SMOOTH; + } else { + return LightMode.FLAT; + } + } + public static class Flags { + boolean hasTexture; + public boolean hasBrightness; + public boolean hasColor; + boolean hasNormals; + + public Flags(byte flags) { + hasTexture = (flags & 1) != 0; + hasBrightness = (flags & 2) != 0; + hasColor = (flags & 4) != 0; + hasNormals = (flags & 8) != 0; + } + + public Flags(boolean hasTexture, boolean hasBrightness, boolean hasColor, boolean hasNormals) { + this.hasTexture = hasTexture; + this.hasBrightness = hasBrightness; + this.hasColor = hasColor; + this.hasNormals = hasNormals; + } + + public byte toByte() { + byte flags = 0; + if(hasTexture) { + flags |= 1; + } + if(hasBrightness) { + flags |= 2; + } + if(hasColor) { + flags |= 4; + } + if(hasNormals) { + flags |= 8; + } + return flags; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/ChunkRenderCache.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/ChunkRenderCache.java new file mode 100644 index 000000000..feec0c4b8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/ChunkRenderCache.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.render.pipeline; + +public class ChunkRenderCache { + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java new file mode 100644 index 000000000..05ce7207c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java @@ -0,0 +1,457 @@ +package me.jellysquid.mods.sodium.client.render.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import me.jellysquid.mods.sodium.client.model.light.LightMode; +import me.jellysquid.mods.sodium.client.model.light.LightPipeline; +import me.jellysquid.mods.sodium.client.model.light.LightPipelineProvider; +import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuad; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadView; +import me.jellysquid.mods.sodium.client.model.quad.ModelQuadViewMutable; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import me.jellysquid.mods.sodium.client.model.quad.properties.ModelQuadFlags; +import me.jellysquid.mods.sodium.client.render.chunk.compile.buffers.ChunkModelBuffers; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import me.jellysquid.mods.sodium.client.util.color.ColorABGR; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import me.jellysquid.mods.sodium.common.util.WorldUtil; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.util.MathHelper; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.common.util.ForgeDirection; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.IFluidBlock; +import org.joml.Vector3d; + +import static org.joml.Math.lerp; + +public class FluidRenderer { + private static final float EPSILON = 0.001f; + private final LightPipelineProvider lpp; + + private final BlockPos.Mutable scratchPos = new BlockPos.Mutable(); + + private final ModelQuadViewMutable quad = new ModelQuad(); + + private final QuadLightData quadLightData = new QuadLightData(); + private boolean useSeparateAo; + private final int[] quadColors = new int[4]; + + public FluidRenderer(LightPipelineProvider lpp) { + int normal = Norm3b.pack(0.0f, 1.0f, 0.0f); + + for (int i = 0; i < 4; i++) { + this.quad.setNormal(i, normal); + } + + this.lpp = lpp; + } + + private boolean isFluidOccluded(IBlockAccess world, int x, int y, int z, ForgeDirection d, Fluid fluid) { + Block block = world.getBlock(x, y, z); + boolean tmp = WorldUtil.getFluid(world.getBlock(x + d.offsetX, y + d.offsetY, z + d.offsetZ)) == fluid; + + if (block.getMaterial().isOpaque()) { + return tmp || block.isSideSolid(world, x, y, z, d); + // fluidlogged or next to water, occlude sides that are solid or the same liquid + // For a liquid block that's always + } + return tmp; + } + + private boolean isSideExposed(IBlockAccess world, int x, int y, int z, ForgeDirection dir, float height) { + BlockPos pos = this.scratchPos.set(x + dir.offsetX, y + dir.offsetY, z + dir.offsetZ); + Block block = world.getBlock(pos.x, pos.y, pos.z); + + if (block.getMaterial().isOpaque()) { + final boolean renderAsFullCube = block.renderAsNormalBlock(); + + // Hoist these checks to avoid allocating the shape below + if (renderAsFullCube) { + // The top face always be inset, so if the shape above is a full cube it can't possibly occlude + return dir == ForgeDirection.UP; + } else { + return true; + } + } + + return true; + } + + public boolean render(IBlockAccess world, WorldSlice slice, Block block, BlockPos pos, ChunkModelBuffers buffers) { + this.useSeparateAo = AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); + + int posX = pos.x; + int posY = pos.y; + int posZ = pos.z; + + Fluid fluid = ((IFluidBlock) block).getFluid(); + + // Check for occluded sides; if everything is occluded, don't render + boolean sfUp = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.UP, fluid); + boolean sfDown = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.DOWN, fluid) || + !this.isSideExposed(world, posX, posY, posZ, ForgeDirection.DOWN, 0.8888889F); + boolean sfNorth = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.NORTH, fluid); + boolean sfSouth = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.SOUTH, fluid); + boolean sfWest = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.WEST, fluid); + boolean sfEast = this.isFluidOccluded(world, posX, posY, posZ, ForgeDirection.EAST, fluid); + + if (sfUp && sfDown && sfEast && sfWest && sfNorth && sfSouth) { + return false; + } + + // sprites[0] should be the still frames, [1] the flowing, [2] the overlay + // Sides 0 and 1 (top and bottom) are still, 2+ flowing. Overlay is null because 1.7.10 probably doesn't use it. + TextureAtlasSprite[] sprites = new TextureAtlasSprite[]{ + (TextureAtlasSprite) fluid.getStillIcon(), + (TextureAtlasSprite) fluid.getFlowingIcon(), + null + }; + boolean hc = fluid.getColor() != 0xffffffff; + + boolean rendered = false; + + float h1 = this.getCornerHeight(world, posX, posY, posZ, fluid); + float h2 = this.getCornerHeight(world, posX, posY, posZ + 1, fluid); + float h3 = this.getCornerHeight(world, posX + 1, posY, posZ + 1, fluid); + float h4 = this.getCornerHeight(world, posX + 1, posY, posZ, fluid); + + float yOffset = sfDown ? 0.0F : EPSILON; + + final ModelQuadViewMutable quad = this.quad; + + LightMode mode = hc && Minecraft.isAmbientOcclusionEnabled() ? LightMode.SMOOTH : LightMode.FLAT; + LightPipeline lighter = this.lpp.getLighter(mode); + + quad.setFlags(0); + + if (!sfUp && this.isSideExposed(world, posX, posY, posZ, ForgeDirection.UP, Math.min(Math.min(h1, h2), Math.min(h3, h4)))) { + h1 -= 0.001F; + h2 -= 0.001F; + h3 -= 0.001F; + h4 -= 0.001F; + + Vector3d velocity = WorldUtil.getVelocity(world, pos.x, pos.y, pos.z, block); + + TextureAtlasSprite sprite; + ModelQuadFacing facing; + float u1, u2, u3, u4; + float v1, v2, v3, v4; + + if (velocity.x == 0.0D && velocity.z == 0.0D) { + sprite = sprites[0]; + facing = ModelQuadFacing.UP; + u1 = sprite.getInterpolatedU(0.0D); + v1 = sprite.getInterpolatedV(0.0D); + u2 = u1; + v2 = sprite.getInterpolatedV(16.0D); + u3 = sprite.getInterpolatedU(16.0D); + v3 = v2; + u4 = u3; + v4 = v1; + } else { + sprite = sprites[1]; + facing = ModelQuadFacing.UNASSIGNED; + float dir = (float) Math.atan2(velocity.z, velocity.x) - (1.5707964f); + float sin = MathHelper.sin(dir) * 0.25F; + float cos = MathHelper.cos(dir) * 0.25F; + u1 = sprite.getInterpolatedU(8.0F + (-cos - sin) * 16.0F); + v1 = sprite.getInterpolatedV(8.0F + (-cos + sin) * 16.0F); + u2 = sprite.getInterpolatedU(8.0F + (-cos + sin) * 16.0F); + v2 = sprite.getInterpolatedV(8.0F + (cos + sin) * 16.0F); + u3 = sprite.getInterpolatedU(8.0F + (cos + sin) * 16.0F); + v3 = sprite.getInterpolatedV(8.0F + (cos - sin) * 16.0F); + u4 = sprite.getInterpolatedU(8.0F + (cos - sin) * 16.0F); + v4 = sprite.getInterpolatedV(8.0F + (-cos - sin) * 16.0F); + } + + float uAvg = (u1 + u2 + u3 + u4) / 4.0F; + float vAvg = (v1 + v2 + v3 + v4) / 4.0F; + float s1 = (float) sprites[0].getIconWidth() / (sprites[0].getMaxU() - sprites[0].getMinU()); + float s2 = (float) sprites[0].getIconHeight() / (sprites[0].getMaxV() - sprites[0].getMinV()); + float s3 = 4.0F / Math.max(s2, s1); + + u1 = lerp(u1, uAvg, s3); + u2 = lerp(u2, uAvg, s3); + u3 = lerp(u3, uAvg, s3); + u4 = lerp(u4, uAvg, s3); + v1 = lerp(v1, vAvg, s3); + v2 = lerp(v2, vAvg, s3); + v3 = lerp(v3, vAvg, s3); + v4 = lerp(v4, vAvg, s3); + + quad.setSprite(sprite); + + this.setVertex(quad, 0, 0.0f, h1, 0.0f, u1, v1); + this.setVertex(quad, 1, 0.0f, h2, 1.0F, u2, v2); + this.setVertex(quad, 2, 1.0F, h3, 1.0F, u3, v3); + this.setVertex(quad, 3, 1.0F, h4, 0.0f, u4, v4); + + this.calculateQuadColors(quad, pos, lighter, ForgeDirection.UP, 1.0F, hc); + this.flushQuad(buffers, quad, facing, false); + + if (WorldUtil.method_15756(slice, this.scratchPos.set(posX, posY + 1, posZ), fluid)) { + this.setVertex(quad, 3, 0.0f, h1, 0.0f, u1, v1); + this.setVertex(quad, 2, 0.0f, h2, 1.0F, u2, v2); + this.setVertex(quad, 1, 1.0F, h3, 1.0F, u3, v3); + this.setVertex(quad, 0, 1.0F, h4, 0.0f, u4, v4); + + this.flushQuad(buffers, quad, ModelQuadFacing.DOWN, true); + } + + rendered = true; + } + + if (!sfDown) { + TextureAtlasSprite sprite = sprites[0]; + + float minU = sprite.getMinU(); + float maxU = sprite.getMaxU(); + float minV = sprite.getMinV(); + float maxV = sprite.getMaxV(); + quad.setSprite(sprite); + + this.setVertex(quad, 0, 0.0f, yOffset, 1.0F, minU, maxV); + this.setVertex(quad, 1, 0.0f, yOffset, 0.0f, minU, minV); + this.setVertex(quad, 2, 1.0F, yOffset, 0.0f, maxU, minV); + this.setVertex(quad, 3, 1.0F, yOffset, 1.0F, maxU, maxV); + + this.calculateQuadColors(quad, pos, lighter, ForgeDirection.DOWN, 1.0F, hc); + this.flushQuad(buffers, quad, ModelQuadFacing.DOWN, false); + + rendered = true; + } + + quad.setFlags(ModelQuadFlags.IS_ALIGNED); + + for (ForgeDirection dir : DirectionUtil.HORIZONTAL_DIRECTIONS) { + float c1; + float c2; + float x1; + float z1; + float x2; + float z2; + + switch (dir) { + case NORTH: + if (sfNorth) { + continue; + } + + c1 = h1; + c2 = h4; + x1 = 0.0f; + x2 = 1.0F; + z1 = 0.001f; + z2 = z1; + break; + case SOUTH: + if (sfSouth) { + continue; + } + + c1 = h3; + c2 = h2; + x1 = 1.0F; + x2 = 0.0f; + z1 = 0.999f; + z2 = z1; + break; + case WEST: + if (sfWest) { + continue; + } + + c1 = h2; + c2 = h1; + x1 = 0.001f; + x2 = x1; + z1 = 1.0F; + z2 = 0.0f; + break; + case EAST: + if (sfEast) { + continue; + } + + c1 = h4; + c2 = h3; + x1 = 0.999f; + x2 = x1; + z1 = 0.0f; + z2 = 1.0F; + break; + default: + continue; + } + + if (this.isSideExposed(world, posX, posY, posZ, dir, Math.max(c1, c2))) { + int adjX = posX + dir.offsetX; + int adjY = posY + dir.offsetY; + int adjZ = posZ + dir.offsetZ; + + TextureAtlasSprite sprite = sprites[1]; + TextureAtlasSprite oSprite = sprites[2]; + + if (oSprite != null) { + Block adjBlock = world.getBlock(adjX, adjY, adjZ); + + if (WorldUtil.shouldDisplayFluidOverlay(adjBlock)) { + // should ignore invisible blocks, barriers, light blocks + // use static water when adjacent block is ice, glass, stained glass, tinted glass + sprite = oSprite; + } + } + + float u1 = sprite.getInterpolatedU(0.0D); + float u2 = sprite.getInterpolatedU(8.0D); + float v1 = sprite.getInterpolatedV((1.0F - c1) * 16.0F * 0.5F); + float v2 = sprite.getInterpolatedV((1.0F - c2) * 16.0F * 0.5F); + float v3 = sprite.getInterpolatedV(8.0D); + + quad.setSprite(sprite); + + this.setVertex(quad, 0, x2, c2, z2, u2, v2); + this.setVertex(quad, 1, x2, yOffset, z2, u2, v3); + this.setVertex(quad, 2, x1, yOffset, z1, u1, v3); + this.setVertex(quad, 3, x1, c1, z1, u1, v1); + + float br = dir.offsetZ != 0 ? 0.8F : 0.6F; + + ModelQuadFacing facing = ModelQuadFacing.fromDirection(dir); + + this.calculateQuadColors(quad, pos, lighter, dir, br, hc); + this.flushQuad(buffers, quad, facing, false); + + if (sprite != oSprite) { + this.setVertex(quad, 0, x1, c1, z1, u1, v1); + this.setVertex(quad, 1, x1, yOffset, z1, u1, v3); + this.setVertex(quad, 2, x2, yOffset, z2, u2, v3); + this.setVertex(quad, 3, x2, c2, z2, u2, v2); + + this.flushQuad(buffers, quad, facing.getOpposite(), true); + } + + rendered = true; + } + } + + return rendered; + } + + private void calculateQuadColors(ModelQuadView quad, BlockPos pos, LightPipeline lighter, ForgeDirection dir, float brightness, boolean colorized) { + QuadLightData light = this.quadLightData; + lighter.calculate(quad, pos, light, null, dir, false); + + int[] biomeColors = null; + + if (colorized) { + // TODO: Sodium - Colorizer + } + + for (int i = 0; i < 4; i++) { + int color = biomeColors != null ? biomeColors[i] : 0xFFFFFFFF; + final float ao = light.br[i] * brightness; + if (useSeparateAo) { + color &= 0x00FFFFFF; + color |= ((int) (ao * 255.0f)) << 24; + } else { + color = ColorABGR.mul(color, ao); + } + this.quadColors[i] = color; + } + } + + private void flushQuad(ChunkModelBuffers buffers, ModelQuadView quad, ModelQuadFacing facing, boolean flip) { + + int vertexIdx, lightOrder; + + if (flip) { + vertexIdx = 3; + lightOrder = -1; + } else { + vertexIdx = 0; + lightOrder = 1; + } + + ModelVertexSink sink = buffers.getSink(facing); + sink.ensureCapacity(4); + + for (int i = 0; i < 4; i++) { + float x = quad.getX(i); + float y = quad.getY(i); + float z = quad.getZ(i); + + int color = this.quadColors[vertexIdx]; + + float u = quad.getTexU(i); + float v = quad.getTexV(i); + + int light = this.quadLightData.lm[vertexIdx]; + + sink.writeQuad(x, y, z, color, u, v, light); + + vertexIdx += lightOrder; + } + + TextureAtlasSprite sprite = quad.rubidium$getSprite(); + + if (sprite != null) { + buffers.getRenderData().addSprite(sprite); + } + + sink.flush(); + } + + private void setVertex(ModelQuadViewMutable quad, int i, float x, float y, float z, float u, float v) { + quad.setX(i, x); + quad.setY(i, y); + quad.setZ(i, z); + quad.setTexU(i, u); + quad.setTexV(i, v); + } + + private float getCornerHeight(IBlockAccess world, int x, int y, int z, Fluid fluid) { + int samples = 0; + float totalHeight = 0.0F; + + for (int i = 0; i < 4; ++i) { + int x2 = x - (i & 1); + int z2 = z - (i >> 1 & 1); + + Block block = world.getBlock(x2, y + 1, z2); + + if (block instanceof IFluidBlock && ((IFluidBlock) block).getFluid() == fluid) { + return 1.0F; + } + + BlockPos pos = this.scratchPos.set(x2, y, z2); + + block = world.getBlock(pos.x, pos.y, pos.z); + int meta = world.getBlockMetadata(pos.x, pos.y, pos.z); + Fluid fluid2 = block instanceof IFluidBlock ? ((IFluidBlock) world.getBlock(pos.x, pos.y, pos.z)).getFluid() : null; + + if (fluid2 == fluid) { + float height = WorldUtil.getFluidHeight(fluid2, meta); + + if (height >= 0.8F) { + totalHeight += height * 10.0F; + samples += 10; + } else { + totalHeight += height; + ++samples; + } + } else if (!block.getMaterial().isSolid()) { + ++samples; + } + } + + return totalHeight / (float) samples; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheLocal.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheLocal.java new file mode 100644 index 000000000..1271dec7c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheLocal.java @@ -0,0 +1,40 @@ +package me.jellysquid.mods.sodium.client.render.pipeline.context; + +import lombok.Getter; +import me.jellysquid.mods.sodium.client.model.light.LightPipelineProvider; +import me.jellysquid.mods.sodium.client.model.light.cache.ArrayLightDataCache; +import me.jellysquid.mods.sodium.client.render.pipeline.BlockRenderer; +import me.jellysquid.mods.sodium.client.render.pipeline.ChunkRenderCache; +import me.jellysquid.mods.sodium.client.render.pipeline.FluidRenderer; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import me.jellysquid.mods.sodium.client.world.cloned.ChunkRenderContext; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; + +public class ChunkRenderCacheLocal extends ChunkRenderCache { + private final ArrayLightDataCache lightDataCache; + + @Getter + private final BlockRenderer blockRenderer; + @Getter + private final FluidRenderer fluidRenderer; + @Getter + private final WorldSlice worldSlice; + + public ChunkRenderCacheLocal(Minecraft client, WorldClient world) { + this.worldSlice = new WorldSlice(world); + this.lightDataCache = new ArrayLightDataCache(this.worldSlice); + + final LightPipelineProvider lightPipelineProvider = new LightPipelineProvider(lightDataCache); + + this.blockRenderer = new BlockRenderer(lightPipelineProvider); + this.fluidRenderer = new FluidRenderer(lightPipelineProvider); + + } + + public void init(ChunkRenderContext context) { + this.lightDataCache.reset(context.getOrigin()); + this.worldSlice.copyData(context); + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheShared.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheShared.java new file mode 100644 index 000000000..38a930a84 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/context/ChunkRenderCacheShared.java @@ -0,0 +1,67 @@ +package me.jellysquid.mods.sodium.client.render.pipeline.context; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import me.jellysquid.mods.sodium.client.model.light.LightPipelineProvider; +import me.jellysquid.mods.sodium.client.model.light.cache.HashLightDataCache; +import me.jellysquid.mods.sodium.client.render.pipeline.BlockRenderer; +import me.jellysquid.mods.sodium.client.render.pipeline.ChunkRenderCache; +import me.jellysquid.mods.sodium.client.world.WorldSlice; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.world.World; + +import java.util.Map; + +public class ChunkRenderCacheShared extends ChunkRenderCache { + private static final Map INSTANCES = new Reference2ObjectOpenHashMap<>(); + + private final BlockRenderer blockRenderer; + private final HashLightDataCache lightCache; + + private ChunkRenderCacheShared(WorldSlice world) { + Minecraft client = Minecraft.getMinecraft(); + + this.lightCache = new HashLightDataCache(world); + LightPipelineProvider lightPipelineProvider = new LightPipelineProvider(this.lightCache); + + this.blockRenderer = new BlockRenderer(lightPipelineProvider); + } + + public BlockRenderer getBlockRenderer() { + return this.blockRenderer; + } + + private void resetCache() { + this.lightCache.clearCache(); + } + + public static ChunkRenderCacheShared getInstance(WorldClient world) { + ChunkRenderCacheShared instance = INSTANCES.get(world); + + if (instance == null) { + throw new IllegalStateException("No global renderer exists"); + } + + return instance; + } + + public static void destroyRenderContext(WorldClient world) { + if (INSTANCES.remove(world) == null) { + throw new IllegalStateException("No render context exists for world: " + world); + } + } + + public static void createRenderContext(WorldClient world) { + if (INSTANCES.containsKey(world)) { + throw new IllegalStateException("Render context already exists for world: " + world); + } + + INSTANCES.put(world, new ChunkRenderCacheShared(new WorldSlice(world))); + } + + public static void resetCaches() { + for (ChunkRenderCacheShared context : INSTANCES.values()) { + context.resetCache(); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteExtended.java new file mode 100644 index 000000000..570f83b13 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteExtended.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.render.texture; + +public interface SpriteExtended { + void markActive(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteUtil.java b/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteUtil.java new file mode 100644 index 000000000..e0b3719c1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/texture/SpriteUtil.java @@ -0,0 +1,11 @@ +package me.jellysquid.mods.sodium.client.render.texture; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +public class SpriteUtil { + public static void markSpriteActive(TextureAtlasSprite sprite) { + if (sprite instanceof SpriteExtended) { + ((SpriteExtended) sprite).markActive(); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/Dim2i.java b/src/main/java/me/jellysquid/mods/sodium/client/util/Dim2i.java new file mode 100644 index 000000000..d8e6458d6 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/Dim2i.java @@ -0,0 +1,51 @@ +package me.jellysquid.mods.sodium.client.util; + +public class Dim2i { + private final int x; + private final int y; + private final int width; + private final int height; + + public Dim2i(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public int getOriginX() { + return this.x; + } + + public int getOriginY() { + return this.y; + } + + public int getWidth() { + return this.width; + } + + public int getHeight() { + return this.height; + } + + public int getLimitX() { + return this.x + this.width; + } + + public int getLimitY() { + return this.y + this.height; + } + + public boolean containsCursor(double x, double y) { + return x >= this.x && x < this.getLimitX() && y >= this.y && y < this.getLimitY(); + } + + public int getCenterX() { + return this.x + (this.width / 2); + } + + public int getCenterY() { + return this.y + (this.height / 2); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/MathUtil.java b/src/main/java/me/jellysquid/mods/sodium/client/util/MathUtil.java new file mode 100644 index 000000000..980058068 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/MathUtil.java @@ -0,0 +1,26 @@ +package me.jellysquid.mods.sodium.client.util; + +public class MathUtil { + /** + * @return True if the specified number is greater than zero and is a power of two, otherwise false + */ + public static boolean isPowerOfTwo(int n) { + return ((n & (n - 1)) == 0); + } + + /** + * @return Hash of the position, replaces a similar method from modern. It's almost certainly not exactly + * equivalent, but I'll be very concerned if it matters + */ + public static long hashPos(int x, int y, int z) { + return cantor(x, cantor(y, z)); + } + + /** + * Maps every positive a and b to a unique int, barring overflow + * Source on Stack Overflow + */ + private static long cantor(long a, long b) { + return (a + b + 1) * (a + b) / 2 + b; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/ModelQuadUtil.java b/src/main/java/me/jellysquid/mods/sodium/client/util/ModelQuadUtil.java new file mode 100644 index 000000000..04442b1e4 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/ModelQuadUtil.java @@ -0,0 +1,80 @@ +package me.jellysquid.mods.sodium.client.util; + +import me.jellysquid.mods.sodium.common.util.DirectionUtil; +import net.minecraftforge.common.util.ForgeDirection; + +/** + * Provides some utilities and constants for interacting with vanilla's model quad vertex format. + * + * This is the current vertex format used by Minecraft for chunk meshes and model quads. Internally, it uses integer + * arrays for store baked quad data, and as such the following table provides both the byte and int indices. + * + * Byte Index Integer Index Name Format Fields + * 0 ..11 0..2 Position 3 floats x, y, z + * 12..15 3 Color 4 unsigned bytes a, r, g, b + * 16..23 4..5 Block Texture 2 floats u, v + * 24..27 6 Light Texture 2 shorts u, v + * 28..30 7 Normal 3 unsigned bytes x, y, z + * 31 Padding 1 byte + */ +public class ModelQuadUtil { + // Integer indices for vertex attributes, useful for accessing baked quad data + public static final int POSITION_INDEX = 0, + COLOR_INDEX = 3, + TEXTURE_INDEX = 4, + LIGHT_INDEX = 6, + NORMAL_INDEX = 7; + + // Size of vertex format in 4-byte integers + public static final int VERTEX_SIZE = 8; + public static final int VERTEX_SIZE_BYTES = VERTEX_SIZE * 4; + + // Cached array of normals for every facing to avoid expensive computation + static final int[] NORMALS = new int[DirectionUtil.ALL_DIRECTIONS.length]; + + static { + for (int i = 0; i < NORMALS.length; i++) { + final ForgeDirection dir = DirectionUtil.ALL_DIRECTIONS[i]; + NORMALS[i] = Norm3b.pack(dir.offsetX, dir.offsetY, dir.offsetZ); + } + } + + /** + * Returns the normal vector for a model quad with the given {@param facing}. + */ + public static int getFacingNormal(ForgeDirection facing) { + return NORMALS[facing.ordinal()]; + } + + public static int getFacingNormal(ForgeDirection facing, int bakedNormal) { + if(!hasNormal(bakedNormal)) + return NORMALS[facing.ordinal()]; + return bakedNormal; + } + + public static boolean hasNormal(int n) { + return (n & 0xFFFFFF) != 0; + } + + /** + * @param vertexIndex The index of the vertex to access + * @return The starting offset of the vertex's attributes + */ + public static int vertexOffset(int vertexIndex) { + return vertexIndex * VERTEX_SIZE; + } + + public static int mergeBakedLight(int packedLight, int calcLight) { + // bail early in most cases + if (packedLight == 0) + return calcLight; + + final int psl = (packedLight >> 16) & 0xFF; + final int csl = (calcLight >> 16) & 0xFF; + final int pbl = (packedLight) & 0xFF; + final int cbl = (calcLight) & 0xFF; + final int bl = Math.max(pbl, cbl); + final int sl = Math.max(psl, csl); + return (sl << 16) | bl; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/Norm3b.java b/src/main/java/me/jellysquid/mods/sodium/client/util/Norm3b.java new file mode 100644 index 000000000..0e2ffbf28 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/Norm3b.java @@ -0,0 +1,82 @@ +package me.jellysquid.mods.sodium.client.util; + +import net.minecraft.util.MathHelper; +import org.joml.Vector3f; +import org.joml.Vector3i; + +/** + * Provides some utilities for working with packed normal vectors. Each getNormal component provides 8 bits of + * precision in the range of [-1.0,1.0]. + * + * | 32 | 24 | 16 | 8 | + * | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 | + * | Padding | X | Y | Z | + */ +public class Norm3b { + /** + * The maximum value of a getNormal's vector component. + */ + private static final float COMPONENT_RANGE = 127.0f; + + /** + * Constant value which can be multiplied with a floating-point vector component to get the normalized value. The + * multiplication is slightly faster than a floating point division, and this code is a hot path which justifies it. + */ + private static final float NORM = 1.0f / COMPONENT_RANGE; + + static int pack(Vector3i norm) { + return pack(norm.x, norm.y, norm.z); + } + + public static int pack(Vector3f dir) { + return pack(dir.x, dir.y, dir.z); + } + + /** + * Packs the specified vector components into a 32-bit integer in XYZ ordering with the 8 bits of padding at the + * end. + * @param x The x component of the getNormal's vector + * @param y The y component of the getNormal's vector + * @param z The z component of the getNormal's vector + */ + public static int pack(float x, float y, float z) { + int normX = encode(x); + int normY = encode(y); + int normZ = encode(z); + + return (normZ << 16) | (normY << 8) | normX; + } + + /** + * Encodes a float in the range of -1.0..1.0 to a normalized unsigned integer in the range of 0..255 which can then + * be passed to graphics memory. + */ + private static int encode(float comp) { + // TODO: is the clamp necessary here? our inputs should always be normalized vector components + return ((int) (MathHelper.clamp_float(comp, -1.0F, 1.0F) * COMPONENT_RANGE) & 255); + } + + /** + * Unpacks the x-component of the packed getNormal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed getNormal + */ + public static float unpackX(int norm) { + return ((byte) (norm & 0xFF)) * NORM; + } + + /** + * Unpacks the y-component of the packed getNormal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed getNormal + */ + public static float unpackY(int norm) { + return ((byte) ((norm >> 8) & 0xFF)) * NORM; + } + + /** + * Unpacks the z-component of the packed getNormal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed getNormal + */ + public static float unpackZ(int norm) { + return ((byte) ((norm >> 16) & 0xFF)) * NORM; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorABGR.java b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorABGR.java new file mode 100644 index 000000000..55a8dda91 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorABGR.java @@ -0,0 +1,84 @@ +package me.jellysquid.mods.sodium.client.util.color; + +/** + * Provides some utilities for packing and unpacking color components from packed integer colors in ABGR format, which + * is used by OpenGL for color vectors. + * + * | 32 | 24 | 16 | 8 | + * | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 | + * | Alpha | Blue | Green | Red | + */ +public class ColorABGR implements ColorU8 { + /** + * Packs the specified color components into ABGR format. + * @param r The red component of the color + * @param g The green component of the color + * @param b The blue component of the color + * @param a The alpha component of the color + */ + public static int pack(int r, int g, int b, int a) { + return (a & 0xFF) << 24 | (b & 0xFF) << 16 | (g & 0xFF) << 8 | (r & 0xFF); + } + + /** + * @see ColorABGR#pack(int, int, int, int) + */ + public static int pack(float r, float g, float b, float a) { + return pack((int) (r * COMPONENT_RANGE), (int) (g * COMPONENT_RANGE), (int) (b * COMPONENT_RANGE), (int) (a * COMPONENT_RANGE)); + } + + /** + * Multiplies the RGB components of the packed ABGR color using the given scale factors. + * @param color The ABGR packed color to be multiplied + * @param rw The red component scale factor + * @param gw The green component scale factor + * @param bw The blue component scale factor + */ + public static int mul(int color, float rw, float gw, float bw) { + float r = unpackRed(color) * rw; + float g = unpackGreen(color) * gw; + float b = unpackBlue(color) * bw; + + return pack((int) r, (int) g, (int) b, 0xFF); + } + + public static int mul(int color, float w) { + return mul(color, w, w, w); + } + + /** + * @param color The packed 32-bit ABGR color to unpack + * @return The red color component in the range of 0..255 + */ + public static int unpackRed(int color) { + return color & 0xFF; + } + + /** + * @param color The packed 32-bit ABGR color to unpack + * @return The green color component in the range of 0..255 + */ + public static int unpackGreen(int color) { + return color >> 8 & 0xFF; + } + + /** + * @param color The packed 32-bit ABGR color to unpack + * @return The blue color component in the range of 0..255 + */ + public static int unpackBlue(int color) { + return color >> 16 & 0xFF; + } + + /** + * @param color The packed 32-bit ABGR color to unpack + * @return The red color component in the range of 0..255 + */ + public static int unpackAlpha(int color) { + return color >> 24 & 0xFF; + } + + public static int pack(float r, float g, float b) { + return pack(r, g, b, 255); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorARGB.java b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorARGB.java new file mode 100644 index 000000000..3790e17fb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorARGB.java @@ -0,0 +1,66 @@ +package me.jellysquid.mods.sodium.client.util.color; + +/** + * Provides some utilities for packing and unpacking color components from packed integer colors in ARGB format. This + * packed format is used by most of Minecraft, but special care must be taken to pack it into ABGR format before passing + * it to OpenGL attributes. + * + * | 32 | 24 | 16 | 8 | + * | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 | + * | Alpha | Red | Green | Blue | + */ +public class ColorARGB implements ColorU8 { + /** + * Packs the specified color components into big-endian format for consumption by OpenGL. + * @param r The red component of the color + * @param g The green component of the color + * @param b The blue component of the color + * @param a The alpha component of the color + */ + public static int pack(int r, int g, int b, int a) { + return (a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF); + } + + /** + * @param color The packed 32-bit ARGB color to unpack + * @return The red color component in the range of 0..255 + */ + public static int unpackAlpha(int color) { + return color >> 24 & 0xFF; + } + + /** + * @param color The packed 32-bit ARGB color to unpack + * @return The red color component in the range of 0..255 + */ + public static int unpackRed(int color) { + return color >> 16 & 0xFF; + } + + /** + * @param color The packed 32-bit ARGB color to unpack + * @return The green color component in the range of 0..255 + */ + public static int unpackGreen(int color) { + return color >> 8 & 0xFF; + } + + /** + * @param color The packed 32-bit ARGB color to unpack + * @return The blue color component in the range of 0..255 + */ + public static int unpackBlue(int color) { + return color & 0xFF; + } + + /** + * Re-packs the ARGB color into a aBGR color with the specified alpha component. + */ + public static int toABGR(int color, int alpha) { + return Integer.reverseBytes(color << 8 | alpha); + } + + public static int toABGR(int color) { + return Integer.reverseBytes(color << 8); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorMixer.java b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorMixer.java new file mode 100644 index 000000000..c0b33ae57 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorMixer.java @@ -0,0 +1,40 @@ +package me.jellysquid.mods.sodium.client.util.color; + +public class ColorMixer { + private static final long MASK1 = 0x00FF00FF; + private static final long MASK2 = 0xFF00FF00; + + /** + * Mixes two ARGB colors using the given ratios. Use {@link ColorMixer#getStartRatio(float)} and + * {@link ColorMixer#getEndRatio(float)} to convert a floating-point ratio into a integer ratio. + * + * This method takes 64-bit inputs to avoid overflows when mixing the alpha channel. The helper method + * {@link ColorMixer#mixARGB(int, int, int, int)} can be used with 32-bit inputs. + * + * @param c1 The first (starting) color to blend with in ARGB format + * @param c2 The second (ending) color to blend with in ARGB format + * @param f1 The ratio of the color {@param c1} as calculated by {@link ColorMixer#getStartRatio(float)} + * @param f2 The ratio of the color {@param c2} as calculated by {@link ColorMixer#getEndRatio(float)} + * @return The result of ((c1 * f1) + (c2 * f2) as an ARGB-encoded color + */ + public static long mixARGB(long c1, long c2, int f1, int f2) { + return ((((((c1 & MASK1) * f1) + ((c2 & MASK1) * f2)) >> 8) & MASK1) | + (((((c1 & MASK2) * f1) + ((c2 & MASK2) * f2)) >> 8) & MASK2)); + } + + /** + * Helper method to convert 32-bit integers to 64-bit integers and back. + * @see ColorMixer#mixARGB(long, long, int, int) + */ + public static int mixARGB(int c1, int c2, int f1, int f2) { + return (int) mixARGB(Integer.toUnsignedLong(c1), Integer.toUnsignedLong(c2), f1, f2); + } + + public static int getStartRatio(float frac) { + return (int) (256 * frac); + } + + public static int getEndRatio(float frac) { + return 256 - getStartRatio(frac); + } +} \ No newline at end of file diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorU8.java b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorU8.java new file mode 100644 index 000000000..779e28d50 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/color/ColorU8.java @@ -0,0 +1,21 @@ +package me.jellysquid.mods.sodium.client.util.color; + +public interface ColorU8 { + /** + * The maximum value of a color component. + */ + float COMPONENT_RANGE = 255.0f; + + /** + * Constant value which can be multiplied with a floating-point color component to get the normalized value. The + * multiplication is slightly faster than a floating point division, and this code is a hot path which justifies it. + */ + float NORM = 1.0f / COMPONENT_RANGE; + + /** + * Normalizes a color component to the range of 0..1. + */ + static float normalize(float v) { + return v * NORM; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/color/FastCubicSampler.java b/src/main/java/me/jellysquid/mods/sodium/client/util/color/FastCubicSampler.java new file mode 100644 index 000000000..8bbc0e535 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/color/FastCubicSampler.java @@ -0,0 +1,94 @@ +package me.jellysquid.mods.sodium.client.util.color; + +import net.minecraft.util.MathHelper; +import org.joml.Vector3d; + +import java.util.function.Function; + +import static org.joml.Math.lerp; + +public class FastCubicSampler { + private static final double[] DENSITY_CURVE = new double[] { 0.0D, 1.0D, 4.0D, 6.0D, 4.0D, 1.0D, 0.0D }; + private static final int DIAMETER = 6; + + private static Vector3d unpackRgb(int rgb) { + return new Vector3d((rgb >> 16 & 255) / 255.0, (rgb >> 8 & 255) / 255.0, (rgb & 255) / 255.0); + } + + public static Vector3d sampleColor(Vector3d pos, ColorFetcher colorFetcher, Function transformer) { + int intX = MathHelper.floor_double(pos.x); + int intY = MathHelper.floor_double(pos.y); + int intZ = MathHelper.floor_double(pos.z); + + int[] values = new int[DIAMETER * DIAMETER * DIAMETER]; + + for(int x = 0; x < DIAMETER; ++x) { + int blockX = (intX - 2) + x; + + for(int y = 0; y < DIAMETER; ++y) { + int blockY = (intY - 2) + y; + + for(int z = 0; z < DIAMETER; ++z) { + int blockZ = (intZ - 2) + z; + + values[index(x, y, z)] = colorFetcher.fetch(blockX, blockY, blockZ); + } + } + } + + // Fast path! Skip blending the colors if all inputs are the same + if (isHomogenousArray(values)) { + // Take the first color if it's homogenous (all elements are the same...) + return transformer.apply(unpackRgb(values[0])); + } + + double deltaX = pos.x - (double)intX; + double deltaY = pos.y - (double)intY; + double deltaZ = pos.z - (double)intZ; + + Vector3d sum = new Vector3d(); + double totalFactor = 0.0D; + + for(int x = 0; x < DIAMETER; ++x) { + double densityX = lerp(DENSITY_CURVE[x + 1], DENSITY_CURVE[x], deltaX); + + for(int y = 0; y < DIAMETER; ++y) { + double densityY = lerp(DENSITY_CURVE[y + 1], DENSITY_CURVE[y], deltaY); + + for(int z = 0; z < DIAMETER; ++z) { + double densityZ = lerp(DENSITY_CURVE[z + 1], DENSITY_CURVE[z], deltaZ); + + double factor = densityX * densityY * densityZ; + totalFactor += factor; + + Vector3d color = transformer.apply(unpackRgb(values[index(x, y, z)])); + sum.add(color.mul(factor)); + } + } + } + + sum.mul(1.0D / totalFactor); + + return sum; + } + + private static int index(int x, int y, int z) { + return (DIAMETER * DIAMETER * z) + (DIAMETER * y) + x; + } + + public interface ColorFetcher { + int fetch(int x, int y, int z); + } + + private static boolean isHomogenousArray(int[] arr) { + int val = arr[0]; + + for (int i = 1; i < arr.length; i++) { + if (arr[i] != val) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/math/FrustumExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/util/math/FrustumExtended.java new file mode 100644 index 000000000..e78fd526c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/math/FrustumExtended.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.util.math; + +public interface FrustumExtended { + boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix3fExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix3fExtended.java new file mode 100644 index 000000000..d1480f766 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix3fExtended.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.util.math; + +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3f; + +public interface Matrix3fExtended { + /** + * Applies the specified rotation to this matrix in-place. + * + * @param quaternion The quaternion to rotate this matrix by + */ +// void rotate(Quaternion quaternion); + + int computeNormal(ForgeDirection dir); + + float transformVecX(float x, float y, float z); + + float transformVecY(float x, float y, float z); + + float transformVecZ(float x, float y, float z); + + default float transformVecX(Vector3f dir) { + return this.transformVecX(dir.x, dir.y, dir.z); + } + + default float transformVecY(Vector3f dir) { + return this.transformVecY(dir.x, dir.y, dir.z); + } + + default float transformVecZ(Vector3f dir) { + return this.transformVecZ(dir.x, dir.y, dir.z); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix4fExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix4fExtended.java new file mode 100644 index 000000000..861ffa86f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/math/Matrix4fExtended.java @@ -0,0 +1,50 @@ +package me.jellysquid.mods.sodium.client.util.math; + + +public interface Matrix4fExtended { + /** + * Applies the specified rotation to this matrix in-place. + * + * @param quaternion The quaternion to rotate this matrix by + */ +// void rotate(Quaternion quaternion); + + /** + * Applies the specified translation to this matrix in-place. + * + * @param x The x-component of the translation + * @param y The y-component of the translation + * @param z The z-component of the translation + */ + void translate(float x, float y, float z); + + /** + * Applies this matrix transformation to the given input vector, returning the x-component. Avoids the lack of + * struct types in Java and allows for allocation-free return. + * @param x The x-component of the vector + * @param y The y-component of the vector + * @param z The z-component of the vector + * @return The x-component of the transformed input vector + */ + float transformVecX(float x, float y, float z); + + /** + * Applies this matrix transformation to the given input vector, returning the y-component. Avoids the lack of + * struct types in Java and allows for allocation-free return. + * @param x The x-component of the vector + * @param y The y-component of the vector + * @param z The z-component of the vector + * @return The y-component of the transformed input vector + */ + float transformVecY(float x, float y, float z); + + /** + * Applies this matrix transformation to the given input vector, returning the z-component. Avoids the lack of + * struct types in Java and allows for allocation-free return. + * @param x The x-component of the vector + * @param y The y-component of the vector + * @param z The z-component of the vector + * @return The z-component of the transformed input vector + */ + float transformVecZ(float x, float y, float z); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/math/MatrixUtil.java b/src/main/java/me/jellysquid/mods/sodium/client/util/math/MatrixUtil.java new file mode 100644 index 000000000..4e4fe0f95 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/math/MatrixUtil.java @@ -0,0 +1,34 @@ +package me.jellysquid.mods.sodium.client.util.math; + +import me.jellysquid.mods.sodium.client.util.Norm3b; +import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Matrix3f; +import org.joml.Matrix4f; + +public class MatrixUtil { + public static int computeNormal(Matrix3f normalMatrix, ForgeDirection facing) { + return ((Matrix3fExtended) (Object) normalMatrix).computeNormal(facing); + } + + public static Matrix4fExtended getExtendedMatrix(Matrix4f matrix) { + return (Matrix4fExtended) (Object) matrix; + } + + public static Matrix3fExtended getExtendedMatrix(Matrix3f matrix) { + return (Matrix3fExtended) (Object) matrix; + } + + public static int transformPackedNormal(int norm, Matrix3f matrix) { + Matrix3fExtended mat = MatrixUtil.getExtendedMatrix(matrix); + + float normX1 = Norm3b.unpackX(norm); + float normY1 = Norm3b.unpackY(norm); + float normZ1 = Norm3b.unpackZ(norm); + + float normX2 = mat.transformVecX(normX1, normY1, normZ1); + float normY2 = mat.transformVecY(normX1, normY1, normZ1); + float normZ2 = mat.transformVecZ(normX1, normY1, normZ1); + + return Norm3b.pack(normX2, normY2, normZ2); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/rand/SplitMixRandom.java b/src/main/java/me/jellysquid/mods/sodium/client/util/rand/SplitMixRandom.java new file mode 100644 index 000000000..5b79bfbb5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/rand/SplitMixRandom.java @@ -0,0 +1,99 @@ +package me.jellysquid.mods.sodium.client.util.rand; + +import it.unimi.dsi.fastutil.HashCommon; + +// SplitMixRandom implementation from DSI Utilities, adopted in a minimal implementation to not +// import Apache Commons. +// +// http://xoshiro.di.unimi.it/ +public class SplitMixRandom { + private static final long PHI = 0x9E3779B97F4A7C15L; + + private long x; + + public SplitMixRandom() { + this(XoRoShiRoRandom.randomSeed()); + } + + public SplitMixRandom(final long seed) { + this.setSeed(seed); + } + + private static long staffordMix13(long z) { + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + + return z ^ (z >>> 31); + } + + private static int staffordMix4Upper32(long z) { + z = (z ^ (z >>> 33)) * 0x62A9D9ED799705F5L; + + return (int) (((z ^ (z >>> 28)) * 0xCB24D0A5C88C35B3L) >>> 32); + } + + public long nextLong() { + return staffordMix13(this.x += PHI); + } + + public int nextInt() { + return staffordMix4Upper32(this.x += PHI); + } + + public int nextInt(final int n) { + return (int) this.nextLong(n); + } + + public long nextLong(final long n) { + if (n <= 0) { + throw new IllegalArgumentException("illegal bound " + n + " (must be positive)"); + } + + long t = staffordMix13(this.x += PHI); + + final long nMinus1 = n - 1; + + if ((n & nMinus1) == 0) { + return t & nMinus1; + } + + long u = t >>> 1; + + while (u + nMinus1 - (t = u % n) < 0) { + u = staffordMix13(this.x += PHI) >>> 1; + } + + return t; + } + + public double nextDouble() { + return (staffordMix13(this.x += PHI) >>> 11) * 0x1.0p-53; + } + + public float nextFloat() { + return (staffordMix4Upper32(this.x += PHI) >>> 8) * 0x1.0p-24f; + } + + public boolean nextBoolean() { + return staffordMix4Upper32(this.x += PHI) < 0; + } + + public void nextBytes(final byte[] bytes) { + int i = bytes.length, n; + + while (i != 0) { + n = Math.min(i, 8); + for (long bits = staffordMix13(this.x += PHI); n-- != 0; bits >>= 8) { + bytes[--i] = (byte) bits; + } + } + } + + public void setSeed(final long seed) { + this.x = HashCommon.murmurHash3(seed); + } + + public void setState(final long state) { + this.x = state; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/rand/XoRoShiRoRandom.java b/src/main/java/me/jellysquid/mods/sodium/client/util/rand/XoRoShiRoRandom.java new file mode 100644 index 000000000..ec6d04f46 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/rand/XoRoShiRoRandom.java @@ -0,0 +1,150 @@ +package me.jellysquid.mods.sodium.client.util.rand; + +import java.util.Random; + +// XoRoShiRo128** implementation from DSI Utilities, adopted in a minimal implementation to not +// import Apache Commons. +// +// http://xoshiro.di.unimi.it/ +public class XoRoShiRoRandom extends Random { + private static final long serialVersionUID = 1L; + + private SplitMixRandom mixer; + private long seed = Long.MIN_VALUE; + private long p0, p1; // The initialization words for the current seed + private long s0, s1; // The current random words + private boolean hasSavedState; // True if we can be quickly reseed by using resetting the words + + private static final SplitMixRandom seedUniquifier = new SplitMixRandom(System.nanoTime()); + + public static long randomSeed() { + final long x; + + synchronized (XoRoShiRoRandom.seedUniquifier) { + x = XoRoShiRoRandom.seedUniquifier.nextLong(); + } + + return x ^ System.nanoTime(); + } + + public XoRoShiRoRandom() { + this(XoRoShiRoRandom.randomSeed()); + } + + public XoRoShiRoRandom(final long seed) { + this.setSeed(seed); + } + + @Override + public long nextLong() { + final long s0 = this.s0; + + long s1 = this.s1; + + final long result = s0 + s1; + + s1 ^= s0; + + this.s0 = Long.rotateLeft(s0, 24) ^ s1 ^ s1 << 16; + this.s1 = Long.rotateLeft(s1, 37); + + return result; + } + + @Override + public int nextInt() { + return (int) this.nextLong(); + } + + @Override + public int nextInt(final int n) { + return (int) this.nextLong(n); + } + + private long nextLong(final long n) { + if (n <= 0) { + throw new IllegalArgumentException("illegal bound " + n + " (must be positive)"); + } + + long t = this.nextLong(); + + final long nMinus1 = n - 1; + + // Shortcut for powers of two--high bits + if ((n & nMinus1) == 0) { + return (t >>> Long.numberOfLeadingZeros(nMinus1)) & nMinus1; + } + + // Rejection-based algorithm to get uniform integers in the general case + long u = t >>> 1; + + while (u + nMinus1 - (t = u % n) < 0) { + u = this.nextLong() >>> 1; + } + + return t; + + } + + @Override + public double nextDouble() { + return Double.longBitsToDouble(0x3FFL << 52 | this.nextLong() >>> 12) - 1.0; + } + + @Override + public float nextFloat() { + return (this.nextLong() >>> 40) * 0x1.0p-24f; + } + + @Override + public boolean nextBoolean() { + return this.nextLong() < 0; + } + + @Override + public void nextBytes(final byte[] bytes) { + int i = bytes.length, n; + + while (i != 0) { + n = Math.min(i, 8); + + for (long bits = this.nextLong(); n-- != 0; bits >>= 8) { + bytes[--i] = (byte) bits; + } + } + } + + @Override + public void setSeed(final long seed) { + // Restore the previous initial state if the seed hasn't changed + // Setting and mixing the seed is expensive, so this saves some CPU cycles + if (this.hasSavedState && this.seed == seed) { + this.s0 = this.p0; + this.s1 = this.p1; + } else { + SplitMixRandom mixer = this.mixer; + + // Avoid allocations of SplitMixRandom + if (mixer == null) { + mixer = this.mixer = new SplitMixRandom(seed); + } else { + mixer.setSeed(seed); + } + + this.s0 = mixer.nextLong(); + this.s1 = mixer.nextLong(); + + this.p0 = this.s0; + this.p1 = this.s1; + + this.seed = seed; + this.hasSavedState = true; + } + } + + public XoRoShiRoRandom setSeedAndReturn(final long seed) { + this.setSeed(seed); + + return this; + } +} \ No newline at end of file diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/task/CancellationSource.java b/src/main/java/me/jellysquid/mods/sodium/client/util/task/CancellationSource.java new file mode 100644 index 000000000..00dff1481 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/task/CancellationSource.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.util.task; + +public interface CancellationSource { + boolean isCancelled(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/ChunkStatusListener.java b/src/main/java/me/jellysquid/mods/sodium/client/world/ChunkStatusListener.java new file mode 100644 index 000000000..7cdbbc4c3 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/ChunkStatusListener.java @@ -0,0 +1,20 @@ +package me.jellysquid.mods.sodium.client.world; + +/** + * Defines a listener that can be attached to a world's chunk manager to receive chunk load and unload events. + */ +public interface ChunkStatusListener { + /** + * Called after a chunk is added to the world and loaded. + * @param x The x-coordinate of the loaded chunk + * @param z The z-coordinate of the loaded chunk + */ + void onChunkAdded(int x, int z); + + /** + * Called after a chunk is removed from the world and unloaded. + * @param x The x-coordiante of the unloaded chunk + * @param z The z-coordinate of the unloaded chunk + */ + void onChunkRemoved(int x, int z); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/ClientWorldExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/world/ClientWorldExtended.java new file mode 100644 index 000000000..47b7fd8af --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/ClientWorldExtended.java @@ -0,0 +1,8 @@ +package me.jellysquid.mods.sodium.client.world; + +public interface ClientWorldExtended { + /** + * @return The world seed used for generating biome data on the client + */ + long getBiomeSeed(); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/WorldSlice.java b/src/main/java/me/jellysquid/mods/sodium/client/world/WorldSlice.java new file mode 100644 index 000000000..c8761bf93 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/WorldSlice.java @@ -0,0 +1,358 @@ +package me.jellysquid.mods.sodium.client.world; + +import java.util.Arrays; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import com.gtnewhorizons.angelica.compat.mojang.CompatMathHelper; +import lombok.Getter; +import me.jellysquid.mods.sodium.client.world.cloned.ChunkRenderContext; +import me.jellysquid.mods.sodium.client.world.cloned.ClonedChunkSection; +import me.jellysquid.mods.sodium.client.world.cloned.ClonedChunkSectionCache; +import net.minecraft.block.Block; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.init.Blocks; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.MathHelper; +import net.minecraft.world.EnumSkyBlock; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraft.world.gen.structure.StructureBoundingBox; +import net.minecraftforge.common.util.ForgeDirection; + +/** + * Takes a slice of world state (block states, biome and light data arrays) and copies the data for use in off-thread + * operations. This allows chunk build tasks to see a consistent snapshot of chunk data at the exact moment the task was + * created. + * + * World slices are not safe to use from multiple threads at once, but the data they contain is safe from modification + * by the main client thread. + * + * Object pooling should be used to avoid huge allocations as this class contains many large arrays. + */ +public class WorldSlice implements IBlockAccess { + // The number of blocks on each axis in a section. + private static final int SECTION_BLOCK_LENGTH = 16; + + // The number of blocks in a section. + private static final int SECTION_BLOCK_COUNT = SECTION_BLOCK_LENGTH * SECTION_BLOCK_LENGTH * SECTION_BLOCK_LENGTH; + + // The radius of blocks around the origin chunk that should be copied. + private static final int NEIGHBOR_BLOCK_RADIUS = 2; + + // The radius of chunks around the origin chunk that should be copied. + private static final int NEIGHBOR_CHUNK_RADIUS = CompatMathHelper.roundUpToMultiple(NEIGHBOR_BLOCK_RADIUS, 16) >> 4; + + // The number of sections on each axis of this slice. + private static final int SECTION_LENGTH = 1 + (NEIGHBOR_CHUNK_RADIUS * 2); + + // The size of the lookup tables used for mapping values to coordinate int pairs. The lookup table size is always + // a power of two so that multiplications can be replaced with simple bit shifts in hot code paths. + private static final int TABLE_LENGTH = CompatMathHelper.smallestEncompassingPowerOfTwo(SECTION_LENGTH); + + // The number of bits needed for each X/Y/Z component in a lookup table. + private static final int TABLE_BITS = Integer.bitCount(TABLE_LENGTH - 1); + + // The array size for the section lookup table. + private static final int SECTION_TABLE_ARRAY_SIZE = TABLE_LENGTH * TABLE_LENGTH * TABLE_LENGTH; + + // The world this slice has copied data from + private final WorldClient world; + + // Local Section->BlockState table. + private final Block[][] blockArrays; + private final int[][] metadataArrays; + + // Local section copies. Read-only. + private ClonedChunkSection[] sections; + + // Biome data for each chunk section + private BiomeGenBase[][] biomeData; + + // The starting point from which this slice captures blocks + private int baseX, baseY, baseZ; + + private final int worldHeight; + + // The chunk origin of this slice + @Getter + private ChunkSectionPos origin; + + public static ChunkRenderContext prepare(World world, ChunkSectionPos origin, ClonedChunkSectionCache sectionCache) { + final Chunk chunk = world.getChunkFromChunkCoords(origin.x, origin.z); + final ExtendedBlockStorage section = chunk.getBlockStorageArray()[origin.y]; + + // If the chunk section is absent or empty, simply terminate now. There will never be anything in this chunk + // section to render, so we need to signal that a chunk render task shouldn't created. This saves a considerable + // amount of time in queueing instant build tasks and greatly accelerates how quickly the world can be loaded. + if (section == null || section.isEmpty()) { + return null; + } + + final StructureBoundingBox volume = new StructureBoundingBox(origin.getMinX() - NEIGHBOR_BLOCK_RADIUS, + origin.getMinY() - NEIGHBOR_BLOCK_RADIUS, + origin.getMinZ() - NEIGHBOR_BLOCK_RADIUS, + origin.getMaxX() + NEIGHBOR_BLOCK_RADIUS, + origin.getMaxY() + NEIGHBOR_BLOCK_RADIUS, + origin.getMaxZ() + NEIGHBOR_BLOCK_RADIUS); + + // The min/max bounds of the chunks copied by this slice + final int minChunkX = origin.x - NEIGHBOR_CHUNK_RADIUS; + final int minChunkY = origin.y - NEIGHBOR_CHUNK_RADIUS; + final int minChunkZ = origin.z - NEIGHBOR_CHUNK_RADIUS; + + final int maxChunkX = origin.x + NEIGHBOR_CHUNK_RADIUS; + final int maxChunkY = origin.y + NEIGHBOR_CHUNK_RADIUS; + final int maxChunkZ = origin.z + NEIGHBOR_CHUNK_RADIUS; + + final ClonedChunkSection[] sections = new ClonedChunkSection[SECTION_TABLE_ARRAY_SIZE]; + + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + for (int chunkY = minChunkY; chunkY <= maxChunkY; chunkY++) { + sections[getLocalSectionIndex(chunkX - minChunkX, chunkY - minChunkY, chunkZ - minChunkZ)] = sectionCache.acquire(chunkX, chunkY, chunkZ); + } + } + } + + return new ChunkRenderContext(origin, sections, volume); + } + + public WorldSlice(WorldClient world) { + this.world = world; + this.worldHeight = world.getHeight(); + + this.sections = new ClonedChunkSection[SECTION_TABLE_ARRAY_SIZE]; + this.blockArrays = new Block[SECTION_TABLE_ARRAY_SIZE][]; + this.metadataArrays = new int[SECTION_TABLE_ARRAY_SIZE][]; + this.biomeData = new BiomeGenBase[SECTION_TABLE_ARRAY_SIZE][]; + + for (int x = 0; x < SECTION_LENGTH; x++) { + for (int y = 0; y < SECTION_LENGTH; y++) { + for (int z = 0; z < SECTION_LENGTH; z++) { + final int i = getLocalSectionIndex(x, y, z); + this.blockArrays[i] = new Block[SECTION_BLOCK_COUNT]; + this.metadataArrays[i] = new int[SECTION_BLOCK_COUNT]; + } + } + } + } + + public void copyData(ChunkRenderContext context) { + this.origin = context.getOrigin(); + this.sections = context.getSections(); + + this.baseX = (this.origin.x - NEIGHBOR_CHUNK_RADIUS) << 4; + this.baseY = (this.origin.y - NEIGHBOR_CHUNK_RADIUS) << 4; + this.baseZ = (this.origin.z - NEIGHBOR_CHUNK_RADIUS) << 4; + Arrays.fill(this.biomeData, null); + + for (int x = 0; x < SECTION_LENGTH; x++) { + for (int y = 0; y < SECTION_LENGTH; y++) { + for (int z = 0; z < SECTION_LENGTH; z++) { + final int idx = getLocalSectionIndex(x, y, z); + + this.unpackBlockData(this.blockArrays[idx], this.metadataArrays[idx], this.sections[idx], context.getVolume()); + this.biomeData[idx] = this.sections[idx].getBiomeData(); + } + } + } + } + + @Override + public int getLightBrightnessForSkyBlocks(int x, int y, int z, int min) { + final int skyBrightness = this.getSkyBlockTypeBrightness(net.minecraft.world.EnumSkyBlock.Sky, x, y, z); + int blockBrightness = this.getSkyBlockTypeBrightness(net.minecraft.world.EnumSkyBlock.Block, x, y, z); + + if (blockBrightness < min) { + blockBrightness = min; + } + + return skyBrightness << 20 | blockBrightness << 4; + } + public int getSkyBlockTypeBrightness(EnumSkyBlock skyBlock, int x, int y, int z) { + y = MathHelper.clamp_int(y, 0, 255); + if (y < 0 || y >= 256 || x < -30000000 || z < -30000000 || x >= 30000000 || z > 30000000) { + return skyBlock.defaultLightValue; + } + if (this.getBlock(x, y, z).getUseNeighborBrightness()) { + int yp = this.getLightLevel(skyBlock, x, y + 1, z); + final int xp = this.getLightLevel(skyBlock, x + 1, y, z); + final int xm = this.getLightLevel(skyBlock, x - 1, y, z); + final int zp = this.getLightLevel(skyBlock, x, y, z + 1); + final int zm = this.getLightLevel(skyBlock, x, y, z - 1); + + if (xp > yp) yp = xp; + if (xm > yp) yp = xm; + if (zp > yp) yp = zp; + if (zm > yp) yp = zm; + + return yp; + } + + return this.getLightLevel(skyBlock, x, y, z); + } + + @Override + public int isBlockProvidingPowerTo(int x, int y, int z, int directionIn) { + return this.getBlock(x, y, z).isProvidingStrongPower(this, x, y, z, directionIn); + } + + @Override + public boolean isAirBlock(int x, int y, int z) { + return this.getBlock(x, y, z).isAir(this, x, y, z); + } + + @Override + public BiomeGenBase getBiomeGenForCoords(int x, int z) { + final int relX = x - this.baseX; + final int relY = 0; + final int relZ = z - this.baseZ; + + BiomeGenBase biome = this.biomeData[getLocalSectionIndex(relX >> 4, relY >> 4, relZ >> 4)] + [(x & 15) | (z & 15) << 4]; + // can be null if biome wasn't generated yet + return biome == null ? BiomeGenBase.plains : biome; + } + + @Override + public int getHeight() { + return this.worldHeight; + } + + @Override + public boolean extendedLevelsInChunkCache() { + return false; + } + + @Override + public boolean isSideSolid(int x, int y, int z, ForgeDirection side, boolean _default) { + return getBlock(x, y, z).isSideSolid(this, x, y, z, side); + } + + private void unpackBlockData(Block[] blocks, int[] metas, ClonedChunkSection section, StructureBoundingBox box) { + if (this.origin.equals(section.getPosition())) { + this.unpackBlockDataZ(blocks, metas, section); + } else { + this.unpackBlockDataR(blocks, metas, section, box); + } + } + + + private static void copyBlocks(Block[] blocks, int[] metas, ClonedChunkSection section, int minBlockY, int maxBlockY, int minBlockZ, int maxBlockZ, int minBlockX, int maxBlockX) { + for (int y = minBlockY; y <= maxBlockY; y++) { + for (int z = minBlockZ; z <= maxBlockZ; z++) { + for (int x = minBlockX; x <= maxBlockX; x++) { + final int blockIdx = getLocalBlockIndex(x & 15, y & 15, z & 15); + blocks[blockIdx] = section.getBlock(x & 15, y & 15, z & 15); + metas[blockIdx] = section.getBlockMetadata(x & 15, y & 15, z & 15); + } + } + } + } + + private void unpackBlockDataR(Block[] blocks, int metas[], ClonedChunkSection section, StructureBoundingBox box) { + ChunkSectionPos pos = section.getPosition(); + + final int minBlockX = Math.max(box.minX, pos.getMinX()); + final int maxBlockX = Math.min(box.maxX, pos.getMaxX()); + + final int minBlockY = Math.max(box.minY, pos.getMinY()); + final int maxBlockY = Math.min(box.maxY, pos.getMaxY()); + + final int minBlockZ = Math.max(box.minZ, pos.getMinZ()); + final int maxBlockZ = Math.min(box.maxZ, pos.getMaxZ()); + + copyBlocks(blocks, metas, section, minBlockY, maxBlockY, minBlockZ, maxBlockZ, minBlockX, maxBlockX); + } + + private void unpackBlockDataZ(Block[] blocks, int[] metas, ClonedChunkSection section) { + // TODO: Look into a faster copy for this? + final ChunkSectionPos pos = section.getPosition(); + + final int minBlockX = pos.getMinX(); + final int maxBlockX = pos.getMaxX(); + + final int minBlockY = pos.getMinY(); + final int maxBlockY = pos.getMaxY(); + + final int minBlockZ = pos.getMinZ(); + final int maxBlockZ = pos.getMaxZ(); + + // TODO: Can this be optimized? + copyBlocks(blocks, metas, section, minBlockY, maxBlockY, minBlockZ, maxBlockZ, minBlockX, maxBlockX); + } + + public Block getBlock(int x, int y, int z) { + final int relX = x - this.baseX; + final int relY = y - this.baseY; + final int relZ = z - this.baseZ; + final Block block = this.blockArrays[getLocalSectionIndex(relX >> 4, relY >> 4, relZ >> 4)][getLocalBlockIndex(relX & 15, relY & 15, relZ & 15)]; + return block == null ? Blocks.air : block; + } + + public Block getBlockRelative(int x, int y, int z) { + return this.blockArrays[getLocalSectionIndex(x >> 4, y >> 4, z >> 4)][getLocalBlockIndex(x & 15, y & 15, z & 15)]; + } + + public int getBlockMetadataRelative(int x, int y, int z) { + return this.metadataArrays[getLocalSectionIndex(x >> 4, y >> 4, z >> 4)][getLocalBlockIndex(x & 15, y & 15, z & 15)]; + } + + @Override + public int getBlockMetadata(int x, int y, int z) { + final int relX = x - this.baseX; + final int relY = y - this.baseY; + final int relZ = z - this.baseZ; + + return this.metadataArrays[getLocalSectionIndex(relX >> 4, relY >> 4, relZ >> 4)][getLocalBlockIndex(relX & 15, relY & 15, relZ & 15)]; + } + + @Override + public TileEntity getTileEntity(int x, int y, int z) { + int relX = x - this.baseX; + int relY = y - this.baseY; + int relZ = z - this.baseZ; + + return this.sections[getLocalSectionIndex(relX >> 4, relY >> 4, relZ >> 4)].getBlockEntity(relX & 15, relY & 15, relZ & 15); + } + + public int getLightLevel(EnumSkyBlock type, int x, int y, int z) { + y = MathHelper.clamp_int(y, 0, 255); + + int relX = x - this.baseX; + int relY = y - this.baseY; + int relZ = z - this.baseZ; + + return this.sections[getLocalSectionIndex(relX >> 4, relY >> 4, relZ >> 4)].getLightLevel(type, relX & 15, relY & 15, relZ & 15); + } + + public static int getLocalBlockIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + public static int getLocalSectionIndex(int x, int y, int z) { + return y << TABLE_BITS << TABLE_BITS | z << TABLE_BITS | x; + } + + public static int getLocalChunkIndex(int x, int z) { + return z << TABLE_BITS | x; + } + + // Modern checks if the sky is darkened, which only happens in the nether. However, I think 1.7.10's hasNoSky is + // close enough and possibly a behavior change between versions. I also don't know why it's rotationally asymmetric + public float getBrightness(ForgeDirection direction, boolean shaded) { + if (!shaded) { + return world.provider.hasNoSky ? 0.9f : 1.0f; + } + return switch (direction) { + case DOWN -> world.provider.hasNoSky ? 0.9f : 0.5f; + case UP -> world.provider.hasNoSky ? 0.9f : 1.0f; + case NORTH, SOUTH -> 0.8f; + case WEST, EAST -> 0.6f; + case UNKNOWN -> 1.0f; + }; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ChunkRenderContext.java b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ChunkRenderContext.java new file mode 100644 index 000000000..19dfd8d01 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ChunkRenderContext.java @@ -0,0 +1,36 @@ +package me.jellysquid.mods.sodium.client.world.cloned; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import net.minecraft.world.gen.structure.StructureBoundingBox; + +public class ChunkRenderContext { + private final ChunkSectionPos origin; + private final ClonedChunkSection[] sections; + private final StructureBoundingBox volume; + + public ChunkRenderContext(ChunkSectionPos origin, ClonedChunkSection[] sections, StructureBoundingBox volume) { + this.origin = origin; + this.sections = sections; + this.volume = volume; + } + + public ClonedChunkSection[] getSections() { + return this.sections; + } + + public ChunkSectionPos getOrigin() { + return this.origin; + } + + public StructureBoundingBox getVolume() { + return this.volume; + } + + public void releaseResources() { + for (ClonedChunkSection section : sections) { + if (section != null) { + section.getBackingCache().release(section); + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSection.java b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSection.java new file mode 100644 index 000000000..e47db970c --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSection.java @@ -0,0 +1,167 @@ +package me.jellysquid.mods.sodium.client.world.cloned; + +import com.gtnewhorizons.angelica.compat.ExtendedBlockStorageExt; +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; +import lombok.Getter; +import net.minecraft.block.Block; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.EnumSkyBlock; +import net.minecraft.world.World; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraft.world.gen.structure.StructureBoundingBox; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ClonedChunkSection { + private static final EnumSkyBlock[] LIGHT_TYPES = EnumSkyBlock.values(); + private static final ExtendedBlockStorage EMPTY_SECTION = new ExtendedBlockStorage(0, false); + + private final AtomicInteger referenceCount = new AtomicInteger(0); + private final ClonedChunkSectionCache backingCache; + + private final Short2ObjectMap tileEntities; + + private ExtendedBlockStorageExt data; + private final World world; + + private ChunkSectionPos pos; + + @Getter + private BiomeGenBase[] biomeData; + + private long lastUsedTimestamp = Long.MAX_VALUE; + + ClonedChunkSection(ClonedChunkSectionCache backingCache, World world) { + this.backingCache = backingCache; + this.world = world; + this.tileEntities = new Short2ObjectOpenHashMap<>(); + } + + public void init(ChunkSectionPos pos) { + final Chunk chunk = world.getChunkFromChunkCoords(pos.x, pos.z); + + if (chunk == null) { + throw new RuntimeException("Couldn't retrieve chunk at " + pos.toChunkPos()); + } + + ExtendedBlockStorage section = getChunkSection(chunk, pos); + + if (section == null /*WorldChunk.EMPTY_SECTION*/ /*ChunkSection.isEmpty(section)*/) { + section = EMPTY_SECTION; + } + + this.pos = pos; + this.data = new ExtendedBlockStorageExt(section); + + this.biomeData = new BiomeGenBase[chunk.getBiomeArray().length]; + + StructureBoundingBox box = new StructureBoundingBox(pos.getMinX(), pos.getMinY(), pos.getMinZ(), pos.getMaxX(), pos.getMaxY(), pos.getMaxZ()); + + this.tileEntities.clear(); + + // Check for tile entities + for(int y = pos.getMinY(); y <= pos.getMaxY(); y++) { + for(int z = pos.getMinZ(); z <= pos.getMaxZ(); z++) { + for(int x = pos.getMinX(); x <= pos.getMaxX(); x++) { + int lX = x & 15, lY = y & 15, lZ = z & 15; + // We have to use this insanity because in 1.7 the tile entity isn't guaranteed to be created + // when the chunk gets scheduled for rendering. So we might have to create it. + // Cloning is done on the main thread so this will not introduce threading issues + Block block = data.getBlockByExtId(lX, lY, lZ); + if(block.hasTileEntity(data.getExtBlockMetadata(lX, lY, lZ))) { + TileEntity tileentity = chunk.func_150806_e(x & 15, y, z & 15); + + if (tileentity != null) + { + this.tileEntities.put(ChunkSectionPos.packLocal(new BlockPos(tileentity.xCoord & 15, tileentity.yCoord & 15, tileentity.zCoord & 15)), tileentity); + } + } + } + } + } + + // Fill biome data + for(int z = pos.getMinZ(); z <= pos.getMaxZ(); z++) { + for(int x = pos.getMinX(); x <= pos.getMaxX(); x++) { + this.biomeData[((z & 15) << 4) | (x & 15)] = world.getBiomeGenForCoords(x, z); + } + } + } + + public Block getBlock(int x, int y, int z) { + return data.getBlockByExtId(x, y, z); + } + + public int getBlockMetadata(int x, int y, int z) { + return data.getExtBlockMetadata(x, y, z); + } + + public int getLightLevel(EnumSkyBlock type, int x, int y, int z) { + if(type == EnumSkyBlock.Sky) { + if(world.provider.hasNoSky) + return 0; + return data.hasSky ? data.getExtSkylightValue(x, y, z) : type.defaultLightValue; + } + return data.getExtBlocklightValue(x, y, z); + } + + public BiomeGenBase getBiomeForNoiseGen(int x, int y, int z) { + return this.biomeData[x | z << 4]; + + } + + public TileEntity getBlockEntity(int x, int y, int z) { + return this.tileEntities.get(packLocal(x, y, z)); + } + + public ChunkSectionPos getPosition() { + return this.pos; + } + + public static boolean isOutOfBuildLimitVertically(int y) { + return y < 0 || y >= 256; + } + + private static ExtendedBlockStorage getChunkSection(Chunk chunk, ChunkSectionPos pos) { + if (!isOutOfBuildLimitVertically(ChunkSectionPos.getBlockCoord(pos.y()))) { + return chunk.getBlockStorageArray()[pos.y]; + } + + return null; + } + + public void acquireReference() { + this.referenceCount.incrementAndGet(); + } + + public boolean releaseReference() { + return this.referenceCount.decrementAndGet() <= 0; + } + + public long getLastUsedTimestamp() { + return this.lastUsedTimestamp; + } + + public void setLastUsedTimestamp(long timestamp) { + this.lastUsedTimestamp = timestamp; + } + + public ClonedChunkSectionCache getBackingCache() { + return this.backingCache; + } + + /** + * @param x The local x-coordinate + * @param y The local y-coordinate + * @param z The local z-coordinate + * @return An index which can be used to key entities or blocks within a chunk + */ + private static short packLocal(int x, int y, int z) { + return (short) (x << 8 | z << 4 | y); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSectionCache.java b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSectionCache.java new file mode 100644 index 000000000..3d2866520 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/world/cloned/ClonedChunkSectionCache.java @@ -0,0 +1,72 @@ +package me.jellysquid.mods.sodium.client.world.cloned; + +import com.gtnewhorizons.angelica.compat.mojang.ChunkSectionPos; +import it.unimi.dsi.fastutil.longs.Long2ReferenceLinkedOpenHashMap; +import net.minecraft.world.World; + +import java.util.concurrent.TimeUnit; + +public class ClonedChunkSectionCache { + private static final int MAX_CACHE_SIZE = 512; /* number of entries */ + private static final long MAX_CACHE_DURATION = TimeUnit.SECONDS.toNanos(5); /* number of nanoseconds */ + + private final World world; + + private final Long2ReferenceLinkedOpenHashMap byPosition = new Long2ReferenceLinkedOpenHashMap<>(); + private long time; // updated once per frame to be the elapsed time since application start + + public ClonedChunkSectionCache(World world) { + this.world = world; + this.time = getMonotonicTimeSource(); + } + + public synchronized void cleanup() { + this.time = getMonotonicTimeSource(); + this.byPosition.values().removeIf(entry -> this.time > (entry.getLastUsedTimestamp() + MAX_CACHE_DURATION)); + } + + public synchronized ClonedChunkSection acquire(int x, int y, int z) { + long key = ChunkSectionPos.asLong(x, y, z); + ClonedChunkSection section = this.byPosition.get(key); + + if (section == null) { + while (this.byPosition.size() >= MAX_CACHE_SIZE) { + this.byPosition.removeFirst(); + } + + section = this.createSection(x, y, z); + } + + section.setLastUsedTimestamp(this.time); + + return section; + } + + private ClonedChunkSection createSection(int x, int y, int z) { + ClonedChunkSection section = this.allocate(); + + ChunkSectionPos pos = ChunkSectionPos.from(x, y, z); + section.init(pos); + + this.byPosition.putAndMoveToLast(pos.asLong(), section); + + return section; + } + + public synchronized void invalidate(int x, int y, int z) { + this.byPosition.remove(ChunkSectionPos.asLong(x, y, z)); + } + + public void release(ClonedChunkSection section) { + + } + + private ClonedChunkSection allocate() { + return new ClonedChunkSection(this, this.world); + } + + private static long getMonotonicTimeSource() { + // Should be monotonic in JDK 17 on sane platforms... + return System.nanoTime(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/config/Option.java b/src/main/java/me/jellysquid/mods/sodium/common/config/Option.java new file mode 100644 index 000000000..79e7ccc83 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/config/Option.java @@ -0,0 +1,63 @@ +package me.jellysquid.mods.sodium.common.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public class Option { + private final String name; + + private Set modDefined = null; + private boolean enabled; + private boolean userDefined; + + public Option(String name, boolean enabled, boolean userDefined) { + this.name = name; + this.enabled = enabled; + this.userDefined = userDefined; + } + + public void setEnabled(boolean enabled, boolean userDefined) { + this.enabled = enabled; + this.userDefined = userDefined; + } + + public void addModOverride(boolean enabled, String modId) { + this.enabled = enabled; + + if (this.modDefined == null) { + this.modDefined = new LinkedHashSet<>(); + } + + this.modDefined.add(modId); + } + + public boolean isEnabled() { + return this.enabled; + } + + public boolean isOverridden() { + return this.isUserDefined() || this.isModDefined(); + } + + public boolean isUserDefined() { + return this.userDefined; + } + + public boolean isModDefined() { + return this.modDefined != null; + } + + public String getName() { + return this.name; + } + + public void clearModsDefiningValue() { + this.modDefined = null; + } + + public Collection getDefiningMods() { + return this.modDefined != null ? Collections.unmodifiableCollection(this.modDefined) : Collections.emptyList(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/config/SodiumConfig.java b/src/main/java/me/jellysquid/mods/sodium/common/config/SodiumConfig.java new file mode 100644 index 000000000..95e0882ae --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/config/SodiumConfig.java @@ -0,0 +1,110 @@ +package me.jellysquid.mods.sodium.common.config; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Documentation of these options: https://github.com/jellysquid3/sodium-fabric/wiki/Configuration-File + */ +public class SodiumConfig { + private static final Logger LOGGER = LogManager.getLogger("EmbeddiumConfig"); + + private final Map options = new HashMap<>(); + + private SodiumConfig() { + + } + + private void readProperties(Properties props) { + for (Map.Entry entry : props.entrySet()) { + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + + Option option = this.options.get(key); + + if (option == null) { + LOGGER.warn("No configuration key exists with name '{}', ignoring", key); + continue; + } + + boolean enabled; + + if (value.equalsIgnoreCase("true")) { + enabled = true; + } else if (value.equalsIgnoreCase("false")) { + enabled = false; + } else { + LOGGER.warn("Invalid value '{}' encountered for configuration key '{}', ignoring", value, key); + continue; + } + + // TODO: Sodium Config +// if(!enabled && FMLEnvironment.production && SYSTEM_OPTIONS.contains(key)) { +// LOGGER.warn("Configuration key '{}' is a required option and cannot be disabled", key); +// continue; +// } + + option.setEnabled(enabled, true); + } + } + + /** + * Loads the configuration file from the specified location. If it does not exist, a new configuration file will be + * created. The file on disk will then be updated to include any new options. + */ + public static SodiumConfig load(File file) { + if (!file.exists()) { + try { + writeDefaultConfig(file); + } catch (IOException e) { + LOGGER.warn("Could not write default configuration file", e); + } + + return new SodiumConfig(); + } + + final Properties props = new Properties(); + + try (FileInputStream fin = new FileInputStream(file)){ + props.load(fin); + } catch (IOException e) { + throw new RuntimeException("Could not load config file", e); + } + + final SodiumConfig config = new SodiumConfig(); + config.readProperties(props); + + return config; + } + + private static void writeDefaultConfig(File file) throws IOException { + final File dir = file.getParentFile(); + + if (!dir.exists()) { + if (!dir.mkdirs()) { + throw new IOException("Could not create parent directories"); + } + } else if (!dir.isDirectory()) { + throw new IOException("The parent file is not a directory"); + } + + try (Writer writer = new FileWriter(file)) { + writer.write("# This is the configuration file for Sodium.\n"); + writer.write("#\n"); + writer.write("# You can find information on editing this file and all the available options here:\n"); + writer.write("# https://github.com/jellysquid3/sodium-fabric/wiki/Configuration-File\n"); + writer.write("#\n"); + writer.write("# By default, this file will be empty except for this notice.\n"); + } + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/DirectionUtil.java b/src/main/java/me/jellysquid/mods/sodium/common/util/DirectionUtil.java new file mode 100644 index 000000000..be03e7245 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/DirectionUtil.java @@ -0,0 +1,17 @@ +package me.jellysquid.mods.sodium.common.util; + +import net.minecraftforge.common.util.ForgeDirection; + +/** + * Contains a number of cached arrays to avoid allocations since calling Enum#values() requires the backing array to + * be cloned every time. + */ +public class DirectionUtil { + public static final ForgeDirection[] ALL_DIRECTIONS = ForgeDirection.VALID_DIRECTIONS; + + public static final int DIRECTION_COUNT = ALL_DIRECTIONS.length; + + // Provides the same order as enumerating ForgeDirection and checking the axis of each value + public static final ForgeDirection[] HORIZONTAL_DIRECTIONS = new ForgeDirection[] { ForgeDirection.NORTH, ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.EAST }; + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/IdTable.java b/src/main/java/me/jellysquid/mods/sodium/common/util/IdTable.java new file mode 100644 index 000000000..7308507c0 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/IdTable.java @@ -0,0 +1,60 @@ +package me.jellysquid.mods.sodium.common.util; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; + +import java.util.Arrays; + +public class IdTable { + private final IntArrayFIFOQueue freeIds = new IntArrayFIFOQueue(); + + private T[] elements; + private int nextId; + private int capacity; + + @SuppressWarnings("unchecked") + public IdTable(int capacity) { + this.elements = (T[]) new Object[capacity]; + this.capacity = capacity; + } + + public int add(T element) { + int id = this.allocateId(); + + if (id >= this.capacity) { + this.grow(); + } + + this.elements[id] = element; + + return id; + } + + private void grow() { + this.elements = Arrays.copyOf(this.elements, this.capacity *= 2); + } + + public void remove(int id) { + this.elements[id] = null; + this.freeIds.enqueue(id); + } + + private int allocateId() { + if (!this.freeIds.isEmpty()) { + return this.freeIds.dequeueInt(); + } + + return this.nextId++; + } + + public T get(int id) { + return this.elements[id]; + } + + public void set(int id, T value) { + this.elements[id] = value; + } + + public Object[] getElements() { + return this.elements; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/ListUtil.java b/src/main/java/me/jellysquid/mods/sodium/common/util/ListUtil.java new file mode 100644 index 000000000..1002fbcc9 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/ListUtil.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.common.util; + +import java.util.Collection; + +public class ListUtil { + public static void updateList(Collection collection, Collection before, Collection after) { + if (!before.isEmpty()) { + collection.removeAll(before); + } + + if (!after.isEmpty()) { + collection.addAll(after); + } + } + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/WorldUtil.java b/src/main/java/me/jellysquid/mods/sodium/common/util/WorldUtil.java new file mode 100644 index 000000000..f7ce2e49b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/WorldUtil.java @@ -0,0 +1,140 @@ +package me.jellysquid.mods.sodium.common.util; + +import com.gtnewhorizons.angelica.compat.mojang.BlockPos; +import net.minecraft.block.Block; +import net.minecraft.block.BlockLiquid; +import net.minecraft.block.material.Material; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.common.util.ForgeDirection; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.IFluidBlock; +import org.joml.Vector3d; + +/** + * Contains methods stripped from BlockState or FluidState that didn't actually need to be there. Technically these + * could be a mixin to Block or Fluid, but that's annoying while not actually providing any benefit. + */ +public class WorldUtil { + + public static Vector3d getVelocity(IBlockAccess world, int x, int y, int z, Block thizz) { + + Vector3d velocity = new Vector3d(); + int decay = getEffectiveFlowDecay(world, x, y, z, thizz); + + for (ForgeDirection dire : DirectionUtil.HORIZONTAL_DIRECTIONS) { + + int adjX = x + dire.offsetX; + int adjZ = z + dire.offsetZ; + + int adjDecay = getEffectiveFlowDecay(world, adjX, y, adjZ, thizz); + + if (adjDecay < 0) { + + if (!world.getBlock(adjX, y, adjZ).getMaterial().blocksMovement()) { + + adjDecay = getEffectiveFlowDecay(world, adjX, y - 1, adjZ, thizz); + + if (adjDecay >= 0) { + + adjDecay -= (decay - 8); + velocity = velocity.add((adjX - x) * adjDecay, 0, (adjZ - z) * adjDecay); + } + } + } else { + + adjDecay -= decay; + velocity = velocity.add((adjX - x) * adjDecay, 0, (adjZ - z) * adjDecay); + } + } + + if (world.getBlockMetadata(x, y, z) >= 8) { + + if (thizz.isBlockSolid(world, x, y, z - 1, 2) + || thizz.isBlockSolid(world, x, y, z + 1, 3) + || thizz.isBlockSolid(world, x - 1, y, z, 4) + || thizz.isBlockSolid(world, x + 1, y, z, 5) + || thizz.isBlockSolid(world, x, y + 1, z - 1, 2) + || thizz.isBlockSolid(world, x, y + 1, z + 1, 3) + || thizz.isBlockSolid(world, x - 1, y + 1, z, 4) + || thizz.isBlockSolid(world, x + 1, y + 1, z, 5)) { + velocity = velocity.normalize().add(0.0D, -6.0D, 0.0D); + } + } + + if (velocity.x == 0 && velocity.y == 0 && velocity.z == 0) + return velocity.zero(); + return velocity.normalize(); + } + + /** + * Returns true if any block in a 3x3x3 cube is not the same fluid and not an opaque full cube. + * Equivalent to FluidState::method_15756 in modern. + */ + public static boolean method_15756(IBlockAccess world, BlockPos pos, Fluid fluid) { + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + Block block = world.getBlock(pos.x, pos.y, pos.z); + if (!block.isOpaqueCube() && getFluid(block) != fluid) { + return true; + } + } + } + + return false; + } + + /** + * Returns fluid height as a percentage of the block; 0 is none and 1 is full. + */ + public static float getFluidHeight(Fluid fluid, int meta) { + return fluid == null ? 0 : 1 - BlockLiquid.getLiquidHeightPercent(meta); + } + + /** + * Returns the flow decay but converts values indicating falling liquid (values >=8) to their effective source block + * value of zero + */ + public static int getEffectiveFlowDecay(IBlockAccess world, int x, int y, int z, Block thiz) { + + if (world.getBlock(x, y, z).getMaterial() != thiz.getMaterial()) { + + return -1; + } else { + + int decay = world.getBlockMetadata(x, y, z); + return decay >= 8 ? 0 : decay; + } + } + + // I believe forge mappings in modern say BreakableBlock, while yarn says TransparentBlock. + // I have a sneaking suspicion isOpaque is neither, but it works for now + public static boolean shouldDisplayFluidOverlay(Block block) { + return !block.getMaterial().isOpaque() || block.getMaterial() == Material.leaves; + } + + public static Fluid getFluid(Block b) { + return b instanceof IFluidBlock ? ((IFluidBlock) b).getFluid() : null; + } + + /** + * Equivalent to method_15748 in 1.16.5 + */ + public static boolean isEmptyOrSame(Fluid fluid, Fluid otherFluid) { + return otherFluid == null || fluid == otherFluid; + } + + /** + * Equivalent to method_15749 in 1.16.5 + */ + public static boolean method_15749(IBlockAccess world, Fluid thiz, BlockPos pos, ForgeDirection dir) { + Block b = world.getBlock(pos.x, pos.y, pos.z); + Fluid f = getFluid(b); + if (f == thiz) { + return false; + } + if (dir == ForgeDirection.UP) { + return true; + } + return b.getMaterial() != Material.ice && b.isSideSolid(world, pos.x, pos.y, pos.z, dir); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/collections/DequeDrain.java b/src/main/java/me/jellysquid/mods/sodium/common/util/collections/DequeDrain.java new file mode 100644 index 000000000..a9685b9a1 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/collections/DequeDrain.java @@ -0,0 +1,22 @@ +package me.jellysquid.mods.sodium.common.util.collections; + +import java.util.Deque; +import java.util.Iterator; + +public class DequeDrain implements Iterator { + private final Deque deque; + + public DequeDrain(Deque deque) { + this.deque = deque; + } + + @Override + public boolean hasNext() { + return !this.deque.isEmpty(); + } + + @Override + public T next() { + return this.deque.remove(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/common/util/collections/FutureDequeDrain.java b/src/main/java/me/jellysquid/mods/sodium/common/util/collections/FutureDequeDrain.java new file mode 100644 index 000000000..7218be3c7 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/common/util/collections/FutureDequeDrain.java @@ -0,0 +1,55 @@ +package me.jellysquid.mods.sodium.common.util.collections; + +import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; + +import java.util.Deque; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; + +public class FutureDequeDrain implements Iterator { + private final Deque> deque; + private T next = null; + + public FutureDequeDrain(Deque> deque) { + this.deque = deque; + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + + findNext(); + + return next != null; + } + + private void findNext() { + while (!deque.isEmpty()) { + CompletableFuture future = deque.remove(); + + try { + AngelicaRenderQueue.managedBlock(future::isDone); + next = future.join(); + return; + } catch (CancellationException e) { + // no-op + } + } + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + T result = next; + next = null; + + return result; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/proxy/ClientProxy.java b/src/main/java/me/jellysquid/mods/sodium/proxy/ClientProxy.java new file mode 100644 index 000000000..bf7092d0f --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/proxy/ClientProxy.java @@ -0,0 +1,49 @@ +package me.jellysquid.mods.sodium.proxy; + +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import jss.notfine.gui.GuiCustomMenu; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages; +import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiVideoSettings; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; + +public class ClientProxy extends CommonProxy { + + @Override + public void preInit(FMLPreInitializationEvent event) { +// FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onGui(GuiScreenEvent.InitGuiEvent.Pre event) { + if(event.gui instanceof GuiVideoSettings eventGui) { + event.setCanceled(true); + if(GuiScreen.isShiftKeyDown()) { + Minecraft.getMinecraft().displayGuiScreen(new GuiCustomMenu(eventGui.parentGuiScreen, SodiumGameOptionPages.general(), + SodiumGameOptionPages.quality(), SodiumGameOptionPages.advanced(), SodiumGameOptionPages.performance())); + } else { + Minecraft.getMinecraft().displayGuiScreen(new SodiumOptionsGUI(eventGui.parentGuiScreen)); + } + } + } + + @Override + public void init(FMLInitializationEvent event) { + // Nothing to do here (yet) + } + + @Override + public void postInit(FMLPostInitializationEvent event) { + // Nothing to do here (yet) + } + + + +} diff --git a/src/main/java/me/jellysquid/mods/sodium/proxy/CommonProxy.java b/src/main/java/me/jellysquid/mods/sodium/proxy/CommonProxy.java new file mode 100644 index 000000000..96363eba5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/proxy/CommonProxy.java @@ -0,0 +1,16 @@ +package me.jellysquid.mods.sodium.proxy; + +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; + +public class CommonProxy { + + public void preInit(FMLPreInitializationEvent event) { + + } + + public void init(FMLInitializationEvent event) {} + + public void postInit(FMLPostInitializationEvent event) {} +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/BatchingDebugMessageHelper.java b/src/main/java/net/coderbot/batchedentityrendering/impl/BatchingDebugMessageHelper.java new file mode 100644 index 000000000..7cdc60b66 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/BatchingDebugMessageHelper.java @@ -0,0 +1,17 @@ +package net.coderbot.batchedentityrendering.impl; + +public class BatchingDebugMessageHelper { + public static String getDebugMessage(DrawCallTrackingRenderBuffers drawTracker) { + int drawCalls = drawTracker.getDrawCalls(); + int renderTypes = drawTracker.getRenderTypes(); + + if (drawCalls > 0) { + int effectivenessTimes10 = renderTypes * 1000 / drawCalls; + float effectiveness = effectivenessTimes10 / 10.0F; + + return drawCalls + " draw calls / " + renderTypes + " render types = " + effectiveness + "% effective"; + } else { + return "(no draw calls)"; + } + } +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/BlendingStateHolder.java b/src/main/java/net/coderbot/batchedentityrendering/impl/BlendingStateHolder.java new file mode 100644 index 000000000..74905dd37 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/BlendingStateHolder.java @@ -0,0 +1,5 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface BlendingStateHolder { + TransparencyType getTransparencyType(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/BufferBuilderExt.java b/src/main/java/net/coderbot/batchedentityrendering/impl/BufferBuilderExt.java new file mode 100644 index 000000000..cc0a98ae1 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/BufferBuilderExt.java @@ -0,0 +1,9 @@ +package net.coderbot.batchedentityrendering.impl; + +import com.gtnewhorizons.angelica.compat.toremove.DrawState; + +import java.nio.ByteBuffer; + +public interface BufferBuilderExt { + +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/BufferSegment.java b/src/main/java/net/coderbot/batchedentityrendering/impl/BufferSegment.java new file mode 100644 index 000000000..237aa54f4 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/BufferSegment.java @@ -0,0 +1,28 @@ +package net.coderbot.batchedentityrendering.impl; + +import com.gtnewhorizons.angelica.compat.toremove.DrawState; +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; +import lombok.Getter; + +import java.nio.ByteBuffer; + +public class BufferSegment { + private final ByteBuffer slice; + @Getter + private final DrawState drawState; + private final RenderLayer type; + + public BufferSegment(ByteBuffer slice, DrawState drawState, RenderLayer type) { + this.slice = slice; + this.drawState = drawState; + this.type = type; + } + + public ByteBuffer getSlice() { + return slice; + } + + public RenderLayer getRenderType() { + return type; + } +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/DrawCallTrackingRenderBuffers.java b/src/main/java/net/coderbot/batchedentityrendering/impl/DrawCallTrackingRenderBuffers.java new file mode 100644 index 000000000..d527db5c8 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/DrawCallTrackingRenderBuffers.java @@ -0,0 +1,7 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface DrawCallTrackingRenderBuffers { + int getDrawCalls(); + int getRenderTypes(); + void resetDrawCounts(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/FlushableMultiBufferSource.java b/src/main/java/net/coderbot/batchedentityrendering/impl/FlushableMultiBufferSource.java new file mode 100644 index 000000000..3d77c02e6 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/FlushableMultiBufferSource.java @@ -0,0 +1,6 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface FlushableMultiBufferSource { + void flushNonTranslucentContent(); + void flushTranslucentContent(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/Groupable.java b/src/main/java/net/coderbot/batchedentityrendering/impl/Groupable.java new file mode 100644 index 000000000..343d094d7 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/Groupable.java @@ -0,0 +1,7 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface Groupable { + void startGroup(); + boolean maybeStartGroup(); + void endGroup(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingBuffer.java b/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingBuffer.java new file mode 100644 index 000000000..7937c4fe8 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingBuffer.java @@ -0,0 +1,6 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface MemoryTrackingBuffer { + int getAllocatedSize(); + int getUsedSize(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingRenderBuffers.java b/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingRenderBuffers.java new file mode 100644 index 000000000..c6ea1641e --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/MemoryTrackingRenderBuffers.java @@ -0,0 +1,7 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface MemoryTrackingRenderBuffers { + int getEntityBufferAllocatedSize(); + int getMiscBufferAllocatedSize(); + int getMaxBegins(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/RenderBuffersExt.java b/src/main/java/net/coderbot/batchedentityrendering/impl/RenderBuffersExt.java new file mode 100644 index 000000000..0226be468 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/RenderBuffersExt.java @@ -0,0 +1,6 @@ +package net.coderbot.batchedentityrendering.impl; + +public interface RenderBuffersExt { + void beginLevelRendering(); + void endLevelRendering(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/TransparencyType.java b/src/main/java/net/coderbot/batchedentityrendering/impl/TransparencyType.java new file mode 100644 index 000000000..6bd124265 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/TransparencyType.java @@ -0,0 +1,27 @@ +package net.coderbot.batchedentityrendering.impl; + +public enum TransparencyType { + /** + * Opaque, non transparent content. + */ + OPAQUE, + /** + * Generally transparent / translucent content. + */ + GENERAL_TRANSPARENT, + /** + * Enchantment glint and crumbling blocks + * These *must* be rendered after their corresponding opaque / transparent parts. + */ + DECAL, + /** + * Water mask, should be drawn after pretty much everything except for translucent terrain and lines. + * Prevents water from appearing inside of boats. + */ + WATER_MASK, + /** + * Block outlines and other debug things that are overlaid on to the world. + * Should be drawn last to avoid weirdness with entity shadows / banners. + */ + LINES +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/WrappableRenderType.java b/src/main/java/net/coderbot/batchedentityrendering/impl/WrappableRenderType.java new file mode 100644 index 000000000..999986295 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/WrappableRenderType.java @@ -0,0 +1,10 @@ +package net.coderbot.batchedentityrendering.impl; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +public interface WrappableRenderType { + /** + * Returns the underlying wrapped RenderType. Might return itself if this RenderType doesn't wrap anything. + */ + RenderLayer unwrap(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/GraphTranslucencyRenderOrderManager.java b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/GraphTranslucencyRenderOrderManager.java new file mode 100644 index 000000000..9d6d18db3 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/GraphTranslucencyRenderOrderManager.java @@ -0,0 +1,140 @@ +package net.coderbot.batchedentityrendering.impl.ordering; + +import de.odysseus.ithaka.digraph.Digraph; +import de.odysseus.ithaka.digraph.Digraphs; +import de.odysseus.ithaka.digraph.MapDigraph; +import de.odysseus.ithaka.digraph.util.fas.FeedbackArcSet; +import de.odysseus.ithaka.digraph.util.fas.FeedbackArcSetPolicy; +import de.odysseus.ithaka.digraph.util.fas.FeedbackArcSetProvider; +import de.odysseus.ithaka.digraph.util.fas.SimpleFeedbackArcSetProvider; +import net.coderbot.batchedentityrendering.impl.BlendingStateHolder; +import net.coderbot.batchedentityrendering.impl.TransparencyType; +import net.coderbot.batchedentityrendering.impl.WrappableRenderType; +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; + +public class GraphTranslucencyRenderOrderManager implements RenderOrderManager { + private final FeedbackArcSetProvider feedbackArcSetProvider; + private final EnumMap> types; + + private boolean inGroup = false; + private final EnumMap currentTypes; + + public GraphTranslucencyRenderOrderManager() { + feedbackArcSetProvider = new SimpleFeedbackArcSetProvider(); + types = new EnumMap<>(TransparencyType.class); + currentTypes = new EnumMap<>(TransparencyType.class); + + for (TransparencyType type : TransparencyType.values()) { + types.put(type, new MapDigraph<>()); + } + } + + private static TransparencyType getTransparencyType(RenderLayer type) { + while (type instanceof WrappableRenderType) { + type = ((WrappableRenderType) type).unwrap(); + } + + if (type instanceof BlendingStateHolder blendingState) { + return blendingState.getTransparencyType(); + } + + // Default to "generally transparent" if we can't figure it out. + return TransparencyType.GENERAL_TRANSPARENT; + } + + public void begin(RenderLayer renderType) { + TransparencyType transparencyType = getTransparencyType(renderType); + Digraph graph = types.get(transparencyType); + graph.add(renderType); + + if (inGroup) { + RenderLayer previous = currentTypes.put(transparencyType, renderType); + + if (previous == null) { + return; + } + + int weight = graph.get(previous, renderType).orElse(0); + weight += 1; + graph.put(previous, renderType, weight); + } + } + + public void startGroup() { + if (inGroup) { + throw new IllegalStateException("Already in a group"); + } + + currentTypes.clear(); + inGroup = true; + } + + public boolean maybeStartGroup() { + if (inGroup) { + return false; + } + + currentTypes.clear(); + inGroup = true; + return true; + } + + public void endGroup() { + if (!inGroup) { + throw new IllegalStateException("Not in a group"); + } + + currentTypes.clear(); + inGroup = false; + } + + @Override + public void reset() { + // TODO: Is reallocation efficient? + types.clear(); + + for (TransparencyType type : TransparencyType.values()) { + types.put(type, new MapDigraph<>()); + } + } + + public Iterable getRenderOrder() { + int layerCount = 0; + + for (Digraph graph : types.values()) { + layerCount += graph.getVertexCount(); + } + + List allLayers = new ArrayList<>(layerCount); + + for (Digraph graph : types.values()) { + // TODO: Make sure that FAS can't become a bottleneck! + // Running NP-hard algorithms in a real time rendering loop might not be an amazing idea. + // This shouldn't be necessary in sane scenes, though, and if there aren't cycles, + // then this *should* be relatively inexpensive, since it'll bail out and return an empty set. + FeedbackArcSet arcSet = + feedbackArcSetProvider.getFeedbackArcSet(graph, graph, FeedbackArcSetPolicy.MIN_WEIGHT); + + if (arcSet.getEdgeCount() > 0) { + // This means that our dependency graph had cycles!!! + // This is very weird and isn't expected - but we try to handle it gracefully anyways. + + // Our feedback arc set algorithm finds some dependency links that can be removed hopefully + // without disrupting the overall order too much. Hopefully it isn't too slow! + for (RenderLayer source : arcSet.vertices()) { + for (RenderLayer target : arcSet.targets(source)) { + graph.remove(source, target); + } + } + } + + allLayers.addAll(Digraphs.toposort(graph, false)); + } + + return allLayers; + } +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/RenderOrderManager.java b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/RenderOrderManager.java new file mode 100644 index 000000000..d04a89bd0 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/RenderOrderManager.java @@ -0,0 +1,12 @@ +package net.coderbot.batchedentityrendering.impl.ordering; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +public interface RenderOrderManager { + void begin(RenderLayer type); + void startGroup(); + boolean maybeStartGroup(); + void endGroup(); + void reset(); + Iterable getRenderOrder(); +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/SimpleRenderOrderManager.java b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/SimpleRenderOrderManager.java new file mode 100644 index 000000000..ff31730a1 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/SimpleRenderOrderManager.java @@ -0,0 +1,39 @@ +package net.coderbot.batchedentityrendering.impl.ordering; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +import java.util.LinkedHashSet; + +public class SimpleRenderOrderManager implements RenderOrderManager { + private final LinkedHashSet renderTypes; + + public SimpleRenderOrderManager() { + renderTypes = new LinkedHashSet<>(); + } + + public void begin(RenderLayer type) { + renderTypes.add(type); + } + + public void startGroup() { + // no-op + } + + public boolean maybeStartGroup() { + // no-op + return false; + } + + public void endGroup() { + // no-op + } + + @Override + public void reset() { + renderTypes.clear(); + } + + public Iterable getRenderOrder() { + return renderTypes; + } +} diff --git a/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/TranslucencyRenderOrderManager.java b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/TranslucencyRenderOrderManager.java new file mode 100644 index 000000000..d0ff81d81 --- /dev/null +++ b/src/main/java/net/coderbot/batchedentityrendering/impl/ordering/TranslucencyRenderOrderManager.java @@ -0,0 +1,76 @@ +package net.coderbot.batchedentityrendering.impl.ordering; + +import net.coderbot.batchedentityrendering.impl.BlendingStateHolder; +import net.coderbot.batchedentityrendering.impl.TransparencyType; +import net.coderbot.batchedentityrendering.impl.WrappableRenderType; +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; + +public class TranslucencyRenderOrderManager implements RenderOrderManager { + private final EnumMap> renderTypes; + + public TranslucencyRenderOrderManager() { + renderTypes = new EnumMap<>(TransparencyType.class); + + for (TransparencyType type : TransparencyType.values()) { + renderTypes.put(type, new LinkedHashSet<>()); + } + } + + private static TransparencyType getTransparencyType(RenderLayer type) { + while (type instanceof WrappableRenderType) { + type = ((WrappableRenderType) type).unwrap(); + } + + if (type instanceof BlendingStateHolder) { + return ((BlendingStateHolder) type).getTransparencyType(); + } + + // Default to "generally transparent" if we can't figure it out. + return TransparencyType.GENERAL_TRANSPARENT; + } + + public void begin(RenderLayer type) { + renderTypes.get(getTransparencyType(type)).add(type); + } + + public void startGroup() { + // no-op + } + + public boolean maybeStartGroup() { + // no-op + return false; + } + + public void endGroup() { + // no-op + } + + @Override + public void reset() { + renderTypes.forEach((type, set) -> { + set.clear(); + }); + } + + public Iterable getRenderOrder() { + int layerCount = 0; + + for (LinkedHashSet set : renderTypes.values()) { + layerCount += set.size(); + } + + List allRenderTypes = new ArrayList<>(layerCount); + + for (LinkedHashSet set : renderTypes.values()) { + allRenderTypes.addAll(set); + } + + return allRenderTypes; + } +} diff --git a/src/main/java/net/coderbot/iris/Iris.java b/src/main/java/net/coderbot/iris/Iris.java new file mode 100644 index 000000000..e00cf5ed9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/Iris.java @@ -0,0 +1,683 @@ +package net.coderbot.iris; + +import com.google.common.base.Throwables; +import com.gtnewhorizons.angelica.AngelicaMod; +import com.gtnewhorizons.angelica.Tags; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.mitchej123.hodgepodge.Common; +import com.mitchej123.hodgepodge.client.HodgepodgeClient; +import com.mojang.realmsclient.gui.ChatFormatting; +import cpw.mods.fml.client.registry.ClientRegistry; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.InputEvent; +import lombok.Getter; +import net.coderbot.iris.config.IrisConfig; +import net.coderbot.iris.gl.GLDebug; +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.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; +import net.coderbot.iris.texture.pbr.PBRTextureManager; +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 org.jetbrains.annotations.NotNull; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GLContext; + +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.Optional; +import java.util.Properties; +import java.util.stream.Stream; +import java.util.zip.ZipError; +import java.util.zip.ZipException; + +public class Iris { + public final boolean isDevelopmentEnvironment; + public static ContextCapabilities capabilities; + + /** + * 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); + + private static Path shaderpacksDirectory; + private static ShaderpackDirectoryManager shaderpacksDirectoryManager; + + private static ShaderPack currentPack; + @Getter + private static String currentPackName; + @Getter + private static boolean initialized; + + private static PipelineManager pipelineManager; + @Getter + private static IrisConfig irisConfig; + private static FileSystem zipFileSystem; + + @Getter + private static final Map 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; + + private static KeyBinding reloadKeybind; + private static KeyBinding toggleShadersKeybind; + private static KeyBinding shaderpackScreenKeybind; + + public static Iris INSTANCE = new Iris(); + + private Iris() { + isDevelopmentEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); + } + + @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. + * + *

This is roughly equivalent to Fabric Loader's ClientModInitializer#onInitializeClient entrypoint, except + * it's entirely cross platform & we get to decide its exact semantics.

+ * + *

This is called right before options are loaded, so we can add key bindings here.

+ */ + 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; + } + + public static void identifyCapabilities() { + capabilities = GLContext.getCapabilities(); + } + /** + * 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; + } + + setDebug(irisConfig.areDebugOptionsEnabled()); + + 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); + } + + 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 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 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; + } + + final Map changedConfigs = tryReadConfigProperties(shaderPackConfigTxt).map(properties -> (Map) (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 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 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"); + } + + // Temp escalation + public static void setDebug(boolean enable) { + GLStateManager.assertMainThread(); + final int success; + if (enable) { + success = GLDebug.setupDebugMessageCallback(); + } else { + success = GLDebug.disableDebugMessages(); + } + logger.info("Debug functionality is " + (enable ? "enabled, logging will be more verbose!" : "disabled.")); + if (Minecraft.getMinecraft().thePlayer != null) { + Minecraft.getMinecraft().thePlayer.sendChatMessage(I18n.format(success != 0 ? (enable + ? "iris.shaders.debug.enabled" + : "iris.shaders.debug.disabled") : "iris.shaders.debug.failure")); + if (success == 2) { + Minecraft.getMinecraft().thePlayer.sendChatMessage(I18n.format("iris.shaders.debug.restart")); + } + } + if(Iris.isInitialized()) { + try { + irisConfig.setDebugEnabled(enable); + irisConfig.save(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static Optional 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 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 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()); + } + // Reload Sodium/Invalidate Chunk Cache + Minecraft.getMinecraft().renderGlobal.loadRenderers(); + } + + /** + * 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; + + if ((level.provider.isHellWorld || level.provider.hasNoSky) || level.provider.dimensionId == -1) { + 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 { + return new DeferredWorldRenderingPipeline(programs); + } 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 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 ChatFormatting color; + String version = getVersion(); + + if (version.endsWith("-development-environment")) { + color = ChatFormatting.GOLD; + version = version.replace("-development-environment", " (Development Environment)"); + } else if (version.endsWith("-dirty") || version.contains("unknown") || version.endsWith("-nogit")) { + color = ChatFormatting.RED; + } else if (version.contains("+rev.")) { + color = ChatFormatting.LIGHT_PURPLE; + } else { + color = ChatFormatting.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("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); + } +} diff --git a/src/main/java/net/coderbot/iris/IrisLogging.java b/src/main/java/net/coderbot/iris/IrisLogging.java new file mode 100644 index 000000000..a14de21ea --- /dev/null +++ b/src/main/java/net/coderbot/iris/IrisLogging.java @@ -0,0 +1,58 @@ +package net.coderbot.iris; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class IrisLogging { + public static boolean ENABLE_SPAM = false; // FabricLoader.getInstance().isDevelopmentEnvironment(); + + private final Logger logger; + + public IrisLogging(String loggerName) { + this.logger = LogManager.getLogger(loggerName); + } + + public void fatal(String fatal) { + this.logger.fatal(fatal); + } + + public void error(String error) { + this.logger.error(error); + } + + public void error(String error, Object... o) { + this.logger.error(error, o); + } + + public void error(String error, Throwable t) { + this.logger.error(error, t); + } + + public void warn(String warning) { + this.logger.warn(warning); + } + + public void warn(String warning, Throwable t) { + this.logger.warn(warning, t); + } + + public void warn(Object... o) { + this.logger.warn(o); + } + + public void info(String info) { + this.logger.info(info); + } + + public void info(String info, Object... o) { + this.logger.info(info, o); + } + + public void debug(String debug) { + this.logger.debug(debug); + } + + public void debug(String debug, Throwable t) { + this.logger.debug(debug, t); + } +} diff --git a/src/main/java/net/coderbot/iris/JomlConversions.java b/src/main/java/net/coderbot/iris/JomlConversions.java new file mode 100644 index 000000000..aee46ff56 --- /dev/null +++ b/src/main/java/net/coderbot/iris/JomlConversions.java @@ -0,0 +1,15 @@ +package net.coderbot.iris; + +import net.minecraft.util.Vec3; +import org.joml.Vector3d; +import org.joml.Vector4f; + +public class JomlConversions { + public static Vector3d fromVec3(Vec3 vec) { + return new Vector3d(vec.xCoord, vec.yCoord, vec.zCoord); + } + + public static Vector4f toJoml(Vector4f v) { + return new Vector4f(v.x(), v.y(), v.z(), v.w()); + } +} diff --git a/src/main/java/net/coderbot/iris/LaunchWarn.java b/src/main/java/net/coderbot/iris/LaunchWarn.java new file mode 100644 index 000000000..8a282d358 --- /dev/null +++ b/src/main/java/net/coderbot/iris/LaunchWarn.java @@ -0,0 +1,43 @@ +package net.coderbot.iris; + +import javax.swing.JOptionPane; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.io.IOException; +import java.net.URI; + +public class LaunchWarn { + public static void main(String[] args) { + // TODO: make this translatable + String message = "This file is the Fabric version of Iris, meant to be installed as a mod. Would you like to get the Iris Installer instead?"; + String fallback = "This file is the Fabric version of Iris, meant to be installed as a mod. Please download the Iris Installer from https://irisshaders.net."; + if (GraphicsEnvironment.isHeadless()) { + System.err.println(fallback); + } else { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ReflectiveOperationException | UnsupportedLookAndFeelException ignored) { + // Ignored + } + + if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + int option = JOptionPane.showOptionDialog(null, message, "Iris Installer", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, null, null, null); + + if (option == JOptionPane.YES_OPTION) { + try { + Desktop.getDesktop().browse(URI.create("https://irisshaders.net")); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + // Fallback for Linux, etc users with no "default" browser + JOptionPane.showMessageDialog(null, fallback); + } + } + + System.exit(0); + } +} diff --git a/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0ConfigImpl.java b/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0ConfigImpl.java new file mode 100644 index 000000000..952bc5183 --- /dev/null +++ b/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0ConfigImpl.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.apiimpl; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.config.IrisConfig; +import net.irisshaders.iris.api.v0.IrisApiConfig; + +import java.io.IOException; + +public class IrisApiV0ConfigImpl implements IrisApiConfig { + @Override + public boolean areShadersEnabled() { + return Iris.getIrisConfig().areShadersEnabled(); + } + + @Override + public void setShadersEnabledAndApply(boolean enabled) { + IrisConfig config = Iris.getIrisConfig(); + + config.setShadersEnabled(enabled); + + try { + config.save(); + } catch (IOException e) { + Iris.logger.error("Error saving configuration file!", e); + } + + try { + Iris.reload(); + } catch (IOException e) { + Iris.logger.error("Error reloading shader pack while applying changes!", e); + } + } +} diff --git a/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0Impl.java b/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0Impl.java new file mode 100644 index 000000000..4312cacbb --- /dev/null +++ b/src/main/java/net/coderbot/iris/apiimpl/IrisApiV0Impl.java @@ -0,0 +1,52 @@ +package net.coderbot.iris.apiimpl; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.FixedFunctionWorldRenderingPipeline; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.shadows.ShadowRenderingState; +import net.irisshaders.iris.api.v0.IrisApi; +import net.irisshaders.iris.api.v0.IrisApiConfig; + + +public class IrisApiV0Impl implements IrisApi { + public static final IrisApiV0Impl INSTANCE = new IrisApiV0Impl(); + private static final IrisApiV0ConfigImpl CONFIG = new IrisApiV0ConfigImpl(); + + @Override + public int getMinorApiRevision() { + return 1; + } + + @Override + public boolean isShaderPackInUse() { + WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + + if (pipeline == null) { + return false; + } + + return !(pipeline instanceof FixedFunctionWorldRenderingPipeline); + } + + @Override + public boolean isRenderingShadowPass() { + return ShadowRenderingState.areShadowsCurrentlyBeingRendered(); + } + + @Override + public Object openMainIrisScreenObj(Object parent) { + return new Object(); + // TODO: GUI + // return new ShaderPackScreen((GuiScreen) parent); + } + + @Override + public String getMainScreenLanguageKey() { + return "options.iris.shaderPackSelection"; + } + + @Override + public IrisApiConfig getConfig() { + return CONFIG; + } +} diff --git a/src/main/java/net/coderbot/iris/block_rendering/BlockMaterialMapping.java b/src/main/java/net/coderbot/iris/block_rendering/BlockMaterialMapping.java new file mode 100644 index 000000000..544af3fa1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/block_rendering/BlockMaterialMapping.java @@ -0,0 +1,108 @@ +package net.coderbot.iris.block_rendering; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.coderbot.iris.shaderpack.materialmap.BlockEntry; +import net.coderbot.iris.shaderpack.materialmap.BlockRenderType; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.util.ResourceLocation; + +import java.util.List; +import java.util.Map; + +public class BlockMaterialMapping { + public static Object2IntMap createBlockStateIdMap(Int2ObjectMap> blockPropertiesMap) { + Object2IntMap blockMatches = new Object2IntOpenHashMap<>(); + + blockPropertiesMap.forEach((intId, entries) -> { + for (BlockEntry entry : entries) { + addBlock(entry, blockMatches, intId); + } + }); + + return blockMatches; + } + + public static Map createBlockTypeMap(Map blockPropertiesMap) { + Map blockTypeIds = new Reference2ReferenceOpenHashMap<>(); + + blockPropertiesMap.forEach((id, blockType) -> { + ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName()); + + Block block = Block.getBlockFromName(resourceLocation.toString()); + + blockTypeIds.put(block, convertBlockToRenderType(blockType)); + }); + + return blockTypeIds; + } + + private static RenderLayer convertBlockToRenderType(BlockRenderType type) { + if (type == null) { + return null; + } + + return switch (type) { + // Everything renders in cutout or translucent in 1.7.10 + case SOLID, CUTOUT, CUTOUT_MIPPED -> RenderLayer.cutout(); + // case SOLID -> RenderLayer.solid(); + // case CUTOUT_MIPPED -> RenderLayer.cutoutMipped(); + case TRANSLUCENT -> RenderLayer.translucent(); + default -> null; + }; + } + + private static void addBlock(BlockEntry entry, Object2IntMap idMap, int intId) { + final NamespacedId id = entry.getId(); + final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName()); + + final Block block = (Block) Block.blockRegistry.getObject(resourceLocation.toString()); + + // If the block doesn't exist, by default the registry will return AIR. That probably isn't what we want. + // TODO: Assuming that Registry.BLOCK.getDefaultId() == "minecraft:air" here + if (block == null || block == Blocks.air) { + return; + } + + idMap.put(block, intId); + +// Set metas = entry.getMetas(); +// // All metas match +// if (metas.isEmpty()) { +// idMap.putIfAbsent(new BlockMatch(block, null), intId); +// return; +// } +// +// // A subset of metas match +// for(int meta : metas) { +// idMap.putIfAbsent(new BlockMatch(block, meta), intId); +// } + } + + // We ignore generics here, the actual types don't matter because we just convert + // them to strings anyways, and the compiler checks just get in the way. + // + // If you're able to rewrite this function without SuppressWarnings, feel free. + // But otherwise it works fine. + // TODO: BlockStateIdMap +// @SuppressWarnings({"rawtypes", "unchecked"}) +// private static boolean checkState(BlockState state, Map, String> expectedValues) { +// for (Map.Entry, String> condition : expectedValues.entrySet()) { +// Property property = condition.getKey(); +// String expectedValue = condition.getValue(); +// +// String actualValue = property.getName(state.getValue(property)); +// +// if (!expectedValue.equals(actualValue)) { +// return false; +// } +// } +// +// return true; +// } +} diff --git a/src/main/java/net/coderbot/iris/block_rendering/BlockRenderingSettings.java b/src/main/java/net/coderbot/iris/block_rendering/BlockRenderingSettings.java new file mode 100644 index 000000000..cbe345740 --- /dev/null +++ b/src/main/java/net/coderbot/iris/block_rendering/BlockRenderingSettings.java @@ -0,0 +1,129 @@ +package net.coderbot.iris.block_rendering; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; +import it.unimi.dsi.fastutil.objects.Object2IntFunction; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import lombok.Getter; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.minecraft.block.Block; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class BlockRenderingSettings { + public static final BlockRenderingSettings INSTANCE = new BlockRenderingSettings(); + + @Getter + private boolean reloadRequired; + private Object2IntMap blockMatches; + private Map blockTypeIds; + private Object2IntFunction entityIds; + private float ambientOcclusionLevel; + private boolean disableDirectionalShading; + private boolean useSeparateAo; + private boolean useExtendedVertexFormat; + + public BlockRenderingSettings() { + reloadRequired = false; + blockMatches = null; + blockTypeIds = null; + ambientOcclusionLevel = 1.0F; + disableDirectionalShading = false; + useSeparateAo = false; + useExtendedVertexFormat = false; + } + + public void clearReloadRequired() { + reloadRequired = false; + } + + @Nullable + public Object2IntMap getBlockMatches() { + return blockMatches; + } + + @Nullable + public Map getBlockTypeIds() { + return blockTypeIds; + } + + @Nullable + public Object2IntFunction getEntityIds() { + return entityIds; + } + + public void setBlockMatches(Object2IntMap blockIds) { + if (this.blockMatches != null && this.blockMatches.equals(blockIds)) { + return; + } + + this.reloadRequired = true; + this.blockMatches = blockIds; + } + + public void setBlockTypeIds(Map blockTypeIds) { + if (this.blockTypeIds != null && this.blockTypeIds.equals(blockTypeIds)) { + return; + } + + this.reloadRequired = true; + this.blockTypeIds = blockTypeIds; + } + + public void setEntityIds(Object2IntFunction entityIds) { + // note: no reload needed, entities are rebuilt every frame. + this.entityIds = entityIds; + } + + public float getAmbientOcclusionLevel() { + return ambientOcclusionLevel; + } + + public void setAmbientOcclusionLevel(float ambientOcclusionLevel) { + if (ambientOcclusionLevel == this.ambientOcclusionLevel) { + return; + } + + this.reloadRequired = true; + this.ambientOcclusionLevel = ambientOcclusionLevel; + } + + public boolean shouldDisableDirectionalShading() { + return disableDirectionalShading; + } + + public void setDisableDirectionalShading(boolean disableDirectionalShading) { + if (disableDirectionalShading == this.disableDirectionalShading) { + return; + } + + this.reloadRequired = true; + this.disableDirectionalShading = disableDirectionalShading; + } + + public boolean shouldUseSeparateAo() { + return useSeparateAo; + } + + public void setUseSeparateAo(boolean useSeparateAo) { + if (useSeparateAo == this.useSeparateAo) { + return; + } + + this.reloadRequired = true; + this.useSeparateAo = useSeparateAo; + } + + public boolean shouldUseExtendedVertexFormat() { + return useExtendedVertexFormat; + } + + public void setUseExtendedVertexFormat(boolean useExtendedVertexFormat) { + if (useExtendedVertexFormat == this.useExtendedVertexFormat) { + return; + } + + this.reloadRequired = true; + this.useExtendedVertexFormat = useExtendedVertexFormat; + } +} diff --git a/src/main/java/net/coderbot/iris/client/IrisDebugScreenHandler.java b/src/main/java/net/coderbot/iris/client/IrisDebugScreenHandler.java new file mode 100644 index 000000000..ffcc3ec53 --- /dev/null +++ b/src/main/java/net/coderbot/iris/client/IrisDebugScreenHandler.java @@ -0,0 +1,76 @@ +package net.coderbot.iris.client; + +import com.gtnewhorizons.angelica.AngelicaMod; +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.mitchej123.hodgepodge.client.HodgepodgeClient; +import cpw.mods.fml.common.eventhandler.EventPriority; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import net.coderbot.iris.Iris; +import net.minecraft.client.Minecraft; +import net.minecraftforge.client.event.RenderGameOverlayEvent; + +import java.lang.management.BufferPoolMXBean; +import java.lang.management.ManagementFactory; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.List; +import java.util.Objects; + +public class IrisDebugScreenHandler { + public static final IrisDebugScreenHandler INSTANCE = new IrisDebugScreenHandler(); + private static final List iris$pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class); + + private static final BufferPoolMXBean iris$directPool; + + static { + BufferPoolMXBean found = null; + + for (BufferPoolMXBean pool : iris$pools) { + if (pool.getName().equals("direct")) { + found = pool; + break; + } + } + + iris$directPool = Objects.requireNonNull(found); + } + + @SubscribeEvent(priority = EventPriority.LOW) + public void onRenderGameOverlayTextEvent(RenderGameOverlayEvent.Text event) { + final Minecraft mc = Minecraft.getMinecraft(); + if (mc.gameSettings.showDebugInfo) { + event.right.add(2, "Direct Buffers: +" + iris$humanReadableByteCountBin(iris$directPool.getMemoryUsed())); + + event.right.add(""); + + if (Iris.getIrisConfig().areShadersEnabled()) { + event.right.add("[" + Iris.MODNAME + "] Shaderpack: " + Iris.getCurrentPackName() + (Iris.isFallback() ? " (fallback)" : "")); + Iris.getCurrentPack().ifPresent(pack -> event.right.add("[" + Iris.MODNAME + "] " + pack.getProfileInfo())); + } else { + event.right.add("[" + Iris.MODNAME + "] Shaders are disabled"); + } + if(AngelicaConfig.speedupAnimations) { + event.right.add(9, "animationsMode: " + AngelicaMod.animationsMode); + } + + Iris.getPipelineManager().getPipeline().ifPresent(pipeline -> pipeline.addDebugText(event.left)); + + } + } + + private static String iris$humanReadableByteCountBin(long bytes) { + long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); + if (absB < 1024) { + return bytes + " B"; + } + long value = absB; + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + value *= Long.signum(bytes); + return String.format("%.3f %ciB", value / 1024.0, ci.current()); + } + +} diff --git a/src/main/java/net/coderbot/iris/config/IrisConfig.java b/src/main/java/net/coderbot/iris/config/IrisConfig.java new file mode 100644 index 000000000..9392cf541 --- /dev/null +++ b/src/main/java/net/coderbot/iris/config/IrisConfig.java @@ -0,0 +1,173 @@ +package net.coderbot.iris.config; + +import net.coderbot.iris.Iris; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Properties; + +/** + * A class dedicated to storing the config values of shaderpacks. Right now it only stores the path to the current shaderpack + */ +public class IrisConfig { + private static final String COMMENT = + "This file stores configuration options for " + Iris.MODNAME + ", such as the currently active shaderpack"; + + /** + * The path to the current shaderpack. Null if the internal shaderpack is being used. + */ + private String shaderPackName; + + /** + * Whether or not shaders are used for rendering. False to disable all shader-based rendering, true to enable it. + */ + private boolean enableShaders; + + /** + * If debug features should be enabled. Gives much more detailed OpenGL error outputs at the cost of performance. + */ + private boolean enableDebugOptions; + + /** + * If the update notification should be disabled or not. + */ + private boolean disableUpdateMessage; + + private final Path propertiesPath; + + public IrisConfig(Path propertiesPath) { + shaderPackName = null; + enableShaders = true; + enableDebugOptions = false; + disableUpdateMessage = false; + this.propertiesPath = propertiesPath; + } + + /** + * Initializes the configuration, loading it if it is present and creating a default config otherwise. + * + * @throws IOException file exceptions + */ + public void initialize() throws IOException { + load(); + if (!Files.exists(propertiesPath)) { + save(); + } + } + + /** + * returns whether or not the current shaderpack is internal + * + * @return if the shaderpack is internal + */ + public boolean isInternal() { + return false; + } + + /** + * Returns the name of the current shaderpack + * + * @return Returns the current shaderpack name - if internal shaders are being used it returns "(internal)" + */ + public Optional getShaderPackName() { + return Optional.ofNullable(shaderPackName); + } + + /** + * Sets the name of the current shaderpack + */ + public void setShaderPackName(String name) { + if (name == null || name.equals("(internal)") || name.isEmpty()) { + this.shaderPackName = null; + } else { + this.shaderPackName = name; + } + } + + /** + * Determines whether or not shaders are used for rendering. + * + * @return False to disable all shader-based rendering, true to enable shader-based rendering. + */ + public boolean areShadersEnabled() { + return enableShaders; + } + + public boolean areDebugOptionsEnabled() { + return enableDebugOptions; + } + + public boolean shouldDisableUpdateMessage() { + return disableUpdateMessage; + } + + public void setDebugEnabled(boolean enabled) { + enableDebugOptions = enabled; + } + + /** + * Sets whether shaders should be used for rendering. + */ + public void setShadersEnabled(boolean enabled) { + this.enableShaders = enabled; + } + + /** + * loads the config file and then populates the string, int, and boolean entries with the parsed entries + * + * @throws IOException if the file cannot be loaded + */ + + public void load() throws IOException { + if (!Files.exists(propertiesPath)) { + return; + } + + Properties properties = new Properties(); + // NB: This uses ISO-8859-1 with unicode escapes as the encoding + try (InputStream is = Files.newInputStream(propertiesPath)) { + properties.load(is); + } + shaderPackName = properties.getProperty("shaderPack"); + enableShaders = !"false".equals(properties.getProperty("enableShaders")); + enableDebugOptions = "true".equals(properties.getProperty("enableDebugOptions")); + disableUpdateMessage = "true".equals(properties.getProperty("disableUpdateMessage")); + // TODO: GUI +// try { +// IrisVideoSettings.shadowDistance = Integer.parseInt(properties.getProperty("maxShadowRenderDistance", "32")); +// } catch (NumberFormatException e) { +// Iris.logger.error("Shadow distance setting reset; value is invalid."); +// IrisVideoSettings.shadowDistance = 32; +// save(); +// } + + if (shaderPackName != null) { + if (shaderPackName.equals("(internal)") || shaderPackName.isEmpty()) { + shaderPackName = null; + } + } + } + + /** + * Serializes the config into a file. Should be called whenever any config values are modified. + * + * @throws IOException file exceptions + */ + public void save() throws IOException { + final Properties properties = new Properties(); + properties.setProperty("shaderPack", getShaderPackName().orElse("")); + properties.setProperty("enableShaders", enableShaders ? "true" : "false"); + properties.setProperty("enableDebugOptions", enableDebugOptions ? "true" : "false"); + properties.setProperty("disableUpdateMessage", disableUpdateMessage ? "true" : "false"); +// properties.setProperty("maxShadowRenderDistance", String.valueOf(IrisVideoSettings.shadowDistance)); + properties.setProperty("maxShadowRenderDistance", String.valueOf(32)); + // NB: This uses ISO-8859-1 with unicode escapes as the encoding + try (OutputStream os = Files.newOutputStream(propertiesPath)) { + properties.store(os, COMMENT); + } + } +} diff --git a/src/main/java/net/coderbot/iris/fantastic/IrisParticleRenderTypes.java b/src/main/java/net/coderbot/iris/fantastic/IrisParticleRenderTypes.java new file mode 100644 index 000000000..1d1a146ce --- /dev/null +++ b/src/main/java/net/coderbot/iris/fantastic/IrisParticleRenderTypes.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.fantastic; + +// TODO: RenderType +//public class IrisParticleRenderTypes { +// public static final ParticleRenderType OPAQUE_TERRAIN = new ParticleRenderType() { +// public void begin(BufferBuilder bufferBuilder, TextureManager textureManager) { +// GL11.glDisable(GL11.GL_BLEND); +// GL11.glDepthMask(true); +// GL11.glAlphaFunc(GL11.GL_GREATER, 0.1F); +// textureManager.bind(TextureAtlas.LOCATION_BLOCKS); +// bufferBuilder.begin(7, DefaultVertexFormat.PARTICLE); +// } +// +// public void end(Tesselator tesselator) { +// tesselator.end(); +// } +// +// public String toString() { +// return "OPAQUE_TERRAIN_SHEET"; +// } +// }; +//} diff --git a/src/main/java/net/coderbot/iris/fantastic/ParticleRenderingPhase.java b/src/main/java/net/coderbot/iris/fantastic/ParticleRenderingPhase.java new file mode 100644 index 000000000..ee5d4980a --- /dev/null +++ b/src/main/java/net/coderbot/iris/fantastic/ParticleRenderingPhase.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.fantastic; + +public enum ParticleRenderingPhase { + EVERYTHING, + OPAQUE, + TRANSLUCENT +} diff --git a/src/main/java/net/coderbot/iris/fantastic/PhasedParticleEngine.java b/src/main/java/net/coderbot/iris/fantastic/PhasedParticleEngine.java new file mode 100644 index 000000000..fee55dce7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/fantastic/PhasedParticleEngine.java @@ -0,0 +1,5 @@ +package net.coderbot.iris.fantastic; + +public interface PhasedParticleEngine { + void setParticleRenderingPhase(ParticleRenderingPhase phase); +} diff --git a/src/main/java/net/coderbot/iris/fantastic/WrappingMultiBufferSource.java b/src/main/java/net/coderbot/iris/fantastic/WrappingMultiBufferSource.java new file mode 100644 index 000000000..076d3a0b9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/fantastic/WrappingMultiBufferSource.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.fantastic; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; +import java.util.function.Function; + +public interface WrappingMultiBufferSource { + void pushWrappingFunction(Function wrappingFunction); + void popWrappingFunction(); + void assertWrapStackEmpty(); +} diff --git a/src/main/java/net/coderbot/iris/features/FeatureFlags.java b/src/main/java/net/coderbot/iris/features/FeatureFlags.java new file mode 100644 index 000000000..a1619e3b9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/features/FeatureFlags.java @@ -0,0 +1,69 @@ +package net.coderbot.iris.features; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.minecraft.client.resources.I18n; +import org.apache.commons.lang3.text.WordUtils; + +import java.util.List; +import java.util.function.BooleanSupplier; + +public enum FeatureFlags { + SEPARATE_HARDWARE_SAMPLERS(() -> true, () -> true), + PER_BUFFER_BLENDING(() -> true, IrisRenderSystem::supportsBufferBlending), + COMPUTE_SHADERS(() -> true, IrisRenderSystem::supportsCompute), + ENTITY_TRANSLUCENT(() -> true, () -> true), + UNKNOWN(() -> false, () -> false); + + private final BooleanSupplier irisRequirement; + private final BooleanSupplier hardwareRequirement; + + FeatureFlags(BooleanSupplier irisRequirement, BooleanSupplier hardwareRequirement) { + this.irisRequirement = irisRequirement; + this.hardwareRequirement = hardwareRequirement; + } + + public static String getInvalidStatus(List invalidFeatureFlags) { + boolean unsupportedHardware = false, unsupportedIris = false; + FeatureFlags[] flags = invalidFeatureFlags.toArray(new FeatureFlags[0]); + for (FeatureFlags flag : flags) { + unsupportedIris |= !flag.irisRequirement.getAsBoolean(); + unsupportedHardware |= !flag.hardwareRequirement.getAsBoolean(); + } + + if (unsupportedIris) { + if (unsupportedHardware) { + return I18n.format("iris.unsupported.irisorpc"); + } + + return I18n.format("iris.unsupported.iris"); + } else if (unsupportedHardware) { + return I18n.format("iris.unsupported.pc"); + } else { + return null; + } + } + + public String getHumanReadableName() { + return WordUtils.capitalize(name().replace("_", " ").toLowerCase()); + } + + public boolean isUsable() { + return irisRequirement.getAsBoolean() && hardwareRequirement.getAsBoolean(); + } + + public static boolean isInvalid(String name) { + try { + return !FeatureFlags.valueOf(name).isUsable(); + } catch (IllegalArgumentException e) { + return true; + } + } + + public static FeatureFlags getValue(String value) { + try { + return FeatureFlags.valueOf(value); + } catch (IllegalArgumentException e) { + return FeatureFlags.UNKNOWN; + } + } +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/InputAvailability.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/InputAvailability.java new file mode 100644 index 000000000..cf0899196 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/InputAvailability.java @@ -0,0 +1,78 @@ +package net.coderbot.iris.gbuffer_overrides.matching; + +public class InputAvailability { + public static final int NUM_VALUES = 8; + + public final boolean texture; + public final boolean lightmap; + public final boolean overlay; + + public InputAvailability(boolean texture, boolean lightmap, boolean overlay) { + this.texture = texture; + this.lightmap = lightmap; + this.overlay = overlay; + } + + public InputAvailability withoutOverlay() { + return new InputAvailability(texture, lightmap, false); + } + + public static InputAvailability unpack(int packed) { + return new InputAvailability((packed & 1) == 1, (packed & 2) == 2, (packed & 4) == 4); + } + + public int pack() { + int packed = 0; + + if (overlay) { + packed |= 4; + } + + if (lightmap) { + packed |= 2; + } + + if (texture) { + packed |= 1; + } + + return packed; + } + + @Override + public String toString() { + return "InputAvailability{" + + "texture=" + texture + + ", lightmap=" + lightmap + + ", overlay=" + overlay + + '}'; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (lightmap ? 1231 : 1237); + result = prime * result + (overlay ? 1231 : 1237); + result = prime * result + (texture ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + InputAvailability other = (InputAvailability) obj; + if (lightmap != other.lightmap) + return false; + if (overlay != other.overlay) + return false; + if (texture != other.texture) + return false; + return true; + } +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/ProgramTable.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/ProgramTable.java new file mode 100644 index 000000000..e581a4830 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/ProgramTable.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.gbuffer_overrides.matching; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +public class ProgramTable { + private final List table = new ArrayList<>(); + + public ProgramTable(BiFunction constructor) { + for (RenderCondition condition : RenderCondition.values()) { + for (int packedAvailability = 0; packedAvailability < InputAvailability.NUM_VALUES; packedAvailability++) { + InputAvailability availability = InputAvailability.unpack(packedAvailability); + + table.add(constructor.apply(condition, availability)); + } + } + } + + // TODO: Remove InputAvailability allocations? + public T match(RenderCondition condition, InputAvailability availability) { + int index = (condition.ordinal() * InputAvailability.NUM_VALUES) + availability.pack(); + + return table.get(index); + } + + public void forEach(Consumer consumer) { + table.forEach(consumer); + } +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/RenderCondition.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/RenderCondition.java new file mode 100644 index 000000000..258336274 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/RenderCondition.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.gbuffer_overrides.matching; + +public enum RenderCondition { + DEFAULT, + SKY, + TERRAIN_OPAQUE, + TERRAIN_TRANSLUCENT, + CLOUDS, + DESTROY, + BLOCK_ENTITIES, + BEACON_BEAM, + ENTITIES, + ENTITIES_TRANSLUCENT, + GLINT, + ENTITY_EYES, + HAND_OPAQUE, + HAND_TRANSLUCENT, + RAIN_SNOW, + WORLD_BORDER, + // NB: Must be last due to implementation details of DeferredWorldRenderingPipeline + SHADOW +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/SpecialCondition.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/SpecialCondition.java new file mode 100644 index 000000000..deeb20933 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/matching/SpecialCondition.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.gbuffer_overrides.matching; + +public enum SpecialCondition { + ENTITY_EYES, + BEACON_BEAM, + GLINT +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/state/RenderTargetStateListener.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/state/RenderTargetStateListener.java new file mode 100644 index 000000000..a3a7a9212 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/state/RenderTargetStateListener.java @@ -0,0 +1,25 @@ +package net.coderbot.iris.gbuffer_overrides.state; + +public interface RenderTargetStateListener { + RenderTargetStateListener NOP = new RenderTargetStateListener() { + @Override + public void beginPostChain() { + + } + + @Override + public void endPostChain() { + + } + + @Override + public void setIsMainBound(boolean bound) { + + } + }; + + void beginPostChain(); + void endPostChain(); + + void setIsMainBound(boolean bound); +} diff --git a/src/main/java/net/coderbot/iris/gbuffer_overrides/state/StateTracker.java b/src/main/java/net/coderbot/iris/gbuffer_overrides/state/StateTracker.java new file mode 100644 index 000000000..bf6c6e982 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gbuffer_overrides/state/StateTracker.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.gbuffer_overrides.state; + +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; + +public class StateTracker { + public static final StateTracker INSTANCE = new StateTracker(); + + // All textures are disabled by default + + // TextureStateShard / TextureUnit.TERRAIN + public boolean albedoSampler; + // LightmapStateShard / TextureUnit.LIGHTMAP + public boolean lightmapSampler; + // OverlayStateShard / TextureUnit.OVERLAY + public boolean overlaySampler; + + public InputAvailability getInputs() { + return new InputAvailability(albedoSampler, + lightmapSampler, + overlaySampler); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/GLDebug.java b/src/main/java/net/coderbot/iris/gl/GLDebug.java new file mode 100644 index 000000000..2e040240d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/GLDebug.java @@ -0,0 +1,330 @@ +/* + * Copyright LWJGL. All rights reserved. Modified by IMS for use in Iris (net.coderbot.iris.gl). + * License terms: https://www.lwjgl.org/license + */ + +package net.coderbot.iris.gl; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.Iris; +import org.lwjgl.opengl.AMDDebugOutput; +import org.lwjgl.opengl.AMDDebugOutputCallback; +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.ARBDebugOutputCallback; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL43; +import org.lwjgl.opengl.KHRDebug; +import org.lwjgl.opengl.KHRDebugCallback; + +import java.io.PrintStream; +import java.util.function.Consumer; + +import static org.lwjgl.opengl.ARBDebugOutput.glDebugMessageCallbackARB; + +public final class GLDebug { + /** + * Sets up debug callbacks + * @return 0 for failure, 1 for success, 2 for restart required. + */ + public static int setupDebugMessageCallback() { + if (Thread.currentThread() != GLStateManager.getMainThread()) { + AngelicaTweaker.LOGGER.warn("setupDebugMessageCallback called from non-main thread!"); + return 0; + } + return setupDebugMessageCallback(System.err); + } + + private static void trace(Consumer output) { + /* + * We can not just use a fixed stacktrace element offset, because some methods + * are intercepted and some are not. So, check the package name. + */ + StackTraceElement[] elems = filterStackTrace(new Throwable(), 4).getStackTrace(); + for (StackTraceElement ste : elems) { + output.accept(ste.toString()); + } + } + + public static Throwable filterStackTrace(Throwable throwable, int offset) { + StackTraceElement[] elems = throwable.getStackTrace(); + StackTraceElement[] filtered = new StackTraceElement[elems.length]; + int j = 0; + for (int i = offset; i < elems.length; i++) { + String className = elems[i].getClassName(); + if (className == null) { + className = ""; + } + filtered[j++] = elems[i]; + } + StackTraceElement[] newElems = new StackTraceElement[j]; + System.arraycopy(filtered, 0, newElems, 0, j); + throwable.setStackTrace(newElems); + return throwable; + } + + private static void printTrace(PrintStream stream) { + trace(new Consumer() { + boolean first = true; + + public void accept(String str) { + if (first) { + printDetail(stream, "Stacktrace", str); + first = false; + } else { + printDetailLine(stream, "Stacktrace", str); + } + } + }); + } + + /** + * Sets up debug callbacks + * @return 0 for failure, 1 for success, 2 for restart required. + */ + public static int setupDebugMessageCallback(PrintStream stream) { + if (Iris.capabilities.OpenGL43 || Iris.capabilities.GL_KHR_debug) { + Iris.logger.info("[GL] Using OpenGL 4.3 for error logging."); + KHRDebugCallback proc = new KHRDebugCallback((source, type, id, severity, message) -> { + stream.println("[LWJGL] OpenGL debug message"); + printDetail(stream, "ID", String.format("0x%X", id)); + printDetail(stream, "Source", getDebugSource(source)); + printDetail(stream, "Type", getDebugType(type)); + printDetail(stream, "Severity", getDebugSeverity(severity)); + printDetail(stream, "Message", message); + printTrace(stream); + }); + GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_HIGH, null, true); + GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false); + GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_LOW, null, false); + GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_NOTIFICATION, null, false); + GL43.glDebugMessageCallback(proc); + + if ((GL11.glGetInteger(GL30.GL_CONTEXT_FLAGS) & GL43.GL_CONTEXT_FLAG_DEBUG_BIT) == 0) { + Iris.logger.warn("[GL] Warning: A non-debug context may not produce any debug output."); + GL11.glEnable(GL43.GL_DEBUG_OUTPUT); + return 2; + } + return 1; + } else if (Iris.capabilities.GL_ARB_debug_output) { + Iris.logger.info("[GL] Using ARB_debug_output for error logging."); + ARBDebugOutputCallback proc = new ARBDebugOutputCallback((source, type, id, severity, message) -> { + stream.println("[LWJGL] ARB_debug_output message"); + printDetail(stream, "ID", String.format("0x%X", id)); + printDetail(stream, "Source", getSourceARB(source)); + printDetail(stream, "Type", getTypeARB(type)); + printDetail(stream, "Severity", getSeverityARB(severity)); + printDetail(stream, "Message", message); + printTrace(stream); + }); + ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_HIGH, null, true); + ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false); + ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_LOW, null, false); + ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_NOTIFICATION, null, false); + ARBDebugOutput.glDebugMessageCallbackARB(proc); + return 1; + } else if (Iris.capabilities.GL_AMD_debug_output) { + Iris.logger.info("[GL] Using AMD_debug_output for error logging."); + AMDDebugOutputCallback proc = new AMDDebugOutputCallback((id, category, severity, message) -> { + stream.println("[LWJGL] AMD_debug_output message"); + printDetail(stream, "ID", String.format("0x%X", id)); + printDetail(stream, "Category", getCategoryAMD(category)); + printDetail(stream, "Severity", getSeverityAMD(severity)); + printDetail(stream, "Message", message); + printTrace(stream); + }); + AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_HIGH, null, true); + AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false); + AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_LOW, null, false); + AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_NOTIFICATION, null, false); + AMDDebugOutput.glDebugMessageCallbackAMD(proc); + return 1; + } else { + Iris.logger.info("[GL] No debug output implementation is available, cannot return debug info."); + return 0; + } + } + + public static int disableDebugMessages() { + if (Iris.capabilities.OpenGL43) { + GL43.glDebugMessageCallback(null); + return 1; + } else if (Iris.capabilities.GL_KHR_debug) { + KHRDebug.glDebugMessageCallback(null); + if (Iris.capabilities.OpenGL30 && (GL11.glGetInteger(GL30.GL_CONTEXT_FLAGS) & 2) == 0) { + GL11.glDisable(GL43.GL_DEBUG_OUTPUT); + } + return 1; + } else if (Iris.capabilities.GL_ARB_debug_output) { + glDebugMessageCallbackARB(null); + return 1; + } else if (Iris.capabilities.GL_AMD_debug_output) { + AMDDebugOutput.glDebugMessageCallbackAMD(null); + return 1; + } else { + Iris.logger.info("[GL] No debug output implementation is available, cannot disable debug info."); + return 0; + } + } + + private static void printDetail(PrintStream stream, String type, String message) { + stream.printf("\t%s: %s\n", type, message); + } + + private static void printDetailLine(PrintStream stream, String type, String message) { + stream.append(" "); + for (int i = 0; i < type.length(); i++) { + stream.append(" "); + } + stream.append(message).append("\n"); + } + + private static String getDebugSource(int source) { + return switch (source) { + case GL43.GL_DEBUG_SOURCE_API -> "API"; + case GL43.GL_DEBUG_SOURCE_WINDOW_SYSTEM -> "WINDOW SYSTEM"; + case GL43.GL_DEBUG_SOURCE_SHADER_COMPILER -> "SHADER COMPILER"; + case GL43.GL_DEBUG_SOURCE_THIRD_PARTY -> "THIRD PARTY"; + case GL43.GL_DEBUG_SOURCE_APPLICATION -> "APPLICATION"; + case GL43.GL_DEBUG_SOURCE_OTHER -> "OTHER"; + default -> String.format("Unknown [0x%X]", source); + }; + } + + private static String getDebugType(int type) { + return switch (type) { + case GL43.GL_DEBUG_TYPE_ERROR -> "ERROR"; + case GL43.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR -> "DEPRECATED BEHAVIOR"; + case GL43.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR -> "UNDEFINED BEHAVIOR"; + case GL43.GL_DEBUG_TYPE_PORTABILITY -> "PORTABILITY"; + case GL43.GL_DEBUG_TYPE_PERFORMANCE -> "PERFORMANCE"; + case GL43.GL_DEBUG_TYPE_OTHER -> "OTHER"; + case GL43.GL_DEBUG_TYPE_MARKER -> "MARKER"; + default -> String.format("Unknown [0x%X]", type); + }; + } + + private static String getDebugSeverity(int severity) { + return switch (severity) { + case GL43.GL_DEBUG_SEVERITY_NOTIFICATION -> "NOTIFICATION"; + case GL43.GL_DEBUG_SEVERITY_HIGH -> "HIGH"; + case GL43.GL_DEBUG_SEVERITY_MEDIUM -> "MEDIUM"; + case GL43.GL_DEBUG_SEVERITY_LOW -> "LOW"; + default -> String.format("Unknown [0x%X]", severity); + }; + } + + private static String getSourceARB(int source) { + return switch (source) { + case ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB -> "API"; + case ARBDebugOutput.GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB -> "WINDOW SYSTEM"; + case ARBDebugOutput.GL_DEBUG_SOURCE_SHADER_COMPILER_ARB -> "SHADER COMPILER"; + case ARBDebugOutput.GL_DEBUG_SOURCE_THIRD_PARTY_ARB -> "THIRD PARTY"; + case ARBDebugOutput.GL_DEBUG_SOURCE_APPLICATION_ARB -> "APPLICATION"; + case ARBDebugOutput.GL_DEBUG_SOURCE_OTHER_ARB -> "OTHER"; + default -> String.format("Unknown [0x%X]", source); + }; + } + + private static String getTypeARB(int type) { + return switch (type) { + case ARBDebugOutput.GL_DEBUG_TYPE_ERROR_ARB -> "ERROR"; + case ARBDebugOutput.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB -> "DEPRECATED BEHAVIOR"; + case ARBDebugOutput.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB -> "UNDEFINED BEHAVIOR"; + case ARBDebugOutput.GL_DEBUG_TYPE_PORTABILITY_ARB -> "PORTABILITY"; + case ARBDebugOutput.GL_DEBUG_TYPE_PERFORMANCE_ARB -> "PERFORMANCE"; + case ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB -> "OTHER"; + default -> String.format("Unknown [0x%X]", type); + }; + } + + private static String getSeverityARB(int severity) { + return switch (severity) { + case ARBDebugOutput.GL_DEBUG_SEVERITY_HIGH_ARB -> "HIGH"; + case ARBDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_ARB -> "MEDIUM"; + case ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB -> "LOW"; + default -> String.format("Unknown [0x%X]", severity); + }; + } + + private static String getCategoryAMD(int category) { + return switch (category) { + case AMDDebugOutput.GL_DEBUG_CATEGORY_API_ERROR_AMD -> "API ERROR"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_WINDOW_SYSTEM_AMD -> "WINDOW SYSTEM"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_DEPRECATION_AMD -> "DEPRECATION"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_UNDEFINED_BEHAVIOR_AMD -> "UNDEFINED BEHAVIOR"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_PERFORMANCE_AMD -> "PERFORMANCE"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_SHADER_COMPILER_AMD -> "SHADER COMPILER"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_APPLICATION_AMD -> "APPLICATION"; + case AMDDebugOutput.GL_DEBUG_CATEGORY_OTHER_AMD -> "OTHER"; + default -> String.format("Unknown [0x%X]", category); + }; + } + + private static String getSeverityAMD(int severity) { + return switch (severity) { + case AMDDebugOutput.GL_DEBUG_SEVERITY_HIGH_AMD -> "HIGH"; + case AMDDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_AMD -> "MEDIUM"; + case AMDDebugOutput.GL_DEBUG_SEVERITY_LOW_AMD -> "LOW"; + default -> String.format("Unknown [0x%X]", severity); + }; + } + + private static DebugState debugState; + + private static interface DebugState { + void nameObject(int id, int object, String name); + void pushGroup(int id, String name); + void popGroup(); + } + + private static class KHRDebugState implements DebugState { + private boolean hasGroup; + + @Override + public void nameObject(int id, int object, String name) { + KHRDebug.glObjectLabel(id, object, name); + } + + @Override + public void pushGroup(int id, String name) { + KHRDebug.glPushDebugGroup(KHRDebug.GL_DEBUG_SOURCE_APPLICATION, id, name); + hasGroup = true; + } + + @Override + public void popGroup() { + if (hasGroup) { + KHRDebug.glPopDebugGroup(); + hasGroup = false; + } + } + } + + private static class UnsupportedDebugState implements DebugState { + @Override + public void nameObject(int id, int object, String name) { + } + + @Override + public void pushGroup(int id, String name) { + } + + @Override + public void popGroup() { + } + } + + public static void initRenderer() { + if (Iris.capabilities.GL_KHR_debug || Iris.capabilities.OpenGL43) { + debugState = new KHRDebugState(); + } else { + debugState = new UnsupportedDebugState(); + } + } + + public static void nameObject(int id, int object, String name) { + debugState.nameObject(id, object, name); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/GlResource.java b/src/main/java/net/coderbot/iris/gl/GlResource.java new file mode 100644 index 000000000..7cf0a4fff --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/GlResource.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.gl; + +public abstract class GlResource { + private final int id; + private boolean isValid; + + protected GlResource(int id) { + this.id = id; + isValid = true; + } + + public final void destroy() { + destroyInternal(); + isValid = false; + } + + protected abstract void destroyInternal(); + + protected void assertValid() { + if (!isValid) { + throw new IllegalStateException("Tried to use a destroyed GlResource"); + } + } + + protected int getGlId() { + assertValid(); + + return id; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/GlVersion.java b/src/main/java/net/coderbot/iris/gl/GlVersion.java new file mode 100644 index 000000000..7a72e007a --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/GlVersion.java @@ -0,0 +1,8 @@ +package net.coderbot.iris.gl; + +public enum GlVersion { + GL_11, + GL_12, + GL_30, + GL_31 +} diff --git a/src/main/java/net/coderbot/iris/gl/IrisRenderSystem.java b/src/main/java/net/coderbot/iris/gl/IrisRenderSystem.java new file mode 100644 index 000000000..75c35a14c --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/IrisRenderSystem.java @@ -0,0 +1,492 @@ +package net.coderbot.iris.gl; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.Iris; +import net.minecraft.client.renderer.OpenGlHelper; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3i; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.ARBDirectStateAccess; +import org.lwjgl.opengl.EXTShaderImageLoadStore; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL40; +import org.lwjgl.opengl.GL42; +import org.lwjgl.opengl.GL43; +import org.lwjgl.opengl.GL45; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * This class is responsible for abstracting calls to OpenGL and asserting that calls are run on the render thread. + */ +public class IrisRenderSystem { + private static DSAAccess dsaState; + private static boolean hasMultibind; + private static boolean supportsCompute; + + public static void initRenderer() { + if (Iris.capabilities.OpenGL45) { + dsaState = new DSACore(); + Iris.logger.info("OpenGL 4.5 detected, enabling DSA."); + } else if (Iris.capabilities.GL_ARB_direct_state_access) { + dsaState = new DSAARB(); + Iris.logger.info("ARB_direct_state_access detected, enabling DSA."); + } else { + dsaState = new DSAUnsupported(); + Iris.logger.info("DSA support not detected."); + } + + hasMultibind = Iris.capabilities.OpenGL45 || Iris.capabilities.GL_ARB_multi_bind; + + supportsCompute = supportsCompute(); + } + + public static void getIntegerv(int pname, IntBuffer params) { + GL11.glGetInteger(pname, params); + } + + public static void getFloatv(int pname, FloatBuffer params) { + GL11.glGetFloat(pname, params); + } + + public static void generateMipmaps(int texture, int mipmapTarget) { + dsaState.generateMipmaps(texture, mipmapTarget); + } + + public static void bindAttributeLocation(int program, int index, CharSequence name) { + GL20.glBindAttribLocation(program, index, name); + } + + public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } + + public static void uniformMatrix4fv(int location, boolean transpose, FloatBuffer matrix) { + GL20.glUniformMatrix4(location, transpose, matrix); + } + + public static void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) { + GL11.glCopyTexImage2D(target, level, internalFormat, x, y, width, height, border); + } + + public static void uniform1f(int location, float v0) { + GL20.glUniform1f(location, v0); + } + + public static void uniform1i(int location, int v0) { + GL20.glUniform1i(location, v0); + } + + public static void uniform2f(int location, float v0, float v1) { + GL20.glUniform2f(location, v0, v1); + } + + public static void uniform2i(int location, int v0, int v1) { + GL20.glUniform2i(location, v0, v1); + } + + public static void uniform3f(int location, float v0, float v1, float v2) { + GL20.glUniform3f(location, v0, v1, v2); + } + + public static void uniform4f(int location, float v0, float v1, float v2, float v3) { + GL20.glUniform4f(location, v0, v1, v2, v3); + } + + public static void uniform4i(int location, int v0, int v1, int v2, int v3) { + GL20.glUniform4i(location, v0, v1, v2, v3); + } + + public static int getAttribLocation(int programId, String name) { + return GL20.glGetAttribLocation(programId, name); + } + + public static int getUniformLocation(int programId, String name) { + return GL20.glGetUniformLocation(programId, name); + } + + public static void texParameteriv(int texture, int target, int pname, IntBuffer params) { + dsaState.texParameteriv(texture, target, pname, params); + } + + public static void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height) { + dsaState.copyTexSubImage2D(destTexture, target, i, i1, i2, i3, i4, width, height); + } + + public static void texParameteri(int texture, int target, int pname, int param) { + dsaState.texParameteri(texture, target, pname, param); + } + + public static void texParameterf(int texture, int target, int pname, float param) { + dsaState.texParameterf(texture, target, pname, param); + } + + public static String getProgramInfoLog(int program) { + return GL20.glGetProgramInfoLog(program, GL20.glGetProgrami(program, GL20.GL_INFO_LOG_LENGTH)); + } + + public static String getShaderInfoLog(int shader) { + return GL20.glGetShaderInfoLog(shader, GL20.glGetShaderi(shader, GL20.GL_INFO_LOG_LENGTH)); + } + + public static void drawBuffers(int framebuffer, IntBuffer buffers) { + dsaState.drawBuffers(framebuffer, buffers); + } + + public static void readBuffer(int framebuffer, int buffer) { + dsaState.readBuffer(framebuffer, buffer); + } + + public static String getActiveUniform(int program, int index, int maxLength, IntBuffer sizeType) { + return GL20.glGetActiveUniform(program, index, maxLength, sizeType); + } + + public static void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels) { + GL11.glReadPixels(x, y, width, height, format, type, pixels); + } + + public static void bufferData(int target, FloatBuffer data, int usage) { + GL15.glBufferData(target, data, usage); + } + + public static int bufferStorage(int target, FloatBuffer data, int usage) { + return dsaState.bufferStorage(target, data, usage); + } + + public static void vertexAttrib4f(int index, float v0, float v1, float v2, float v3) { + GL20.glVertexAttrib4f(index, v0, v1, v2, v3); + } + + public static void detachShader(int program, int shader) { + GL20.glDetachShader(program, shader); + } + + public static void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { + dsaState.framebufferTexture2D(fb, fbtarget, attachment, target, texture, levels); + } + + public static int getTexParameteri(int texture, int target, int pname) { + return dsaState.getTexParameteri(texture, target, pname); + } + + public static void bindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { + if (Iris.capabilities.OpenGL42) { + GL42.glBindImageTexture(unit, texture, level, layered, layer, access, format); + } else { + EXTShaderImageLoadStore.glBindImageTextureEXT(unit, texture, level, layered, layer, access, format); + } + } + + public static int getMaxImageUnits() { + if (Iris.capabilities.OpenGL42) { + return GL11.glGetInteger(GL42.GL_MAX_IMAGE_UNITS); + } else if (Iris.capabilities.GL_EXT_shader_image_load_store) { + return GL11.glGetInteger(EXTShaderImageLoadStore.GL_MAX_IMAGE_UNITS_EXT); + } else { + return 0; + } + } + + public static void getProgramiv(int program, int value, IntBuffer storage) { + GL20.glGetProgram(program, value, storage); + } + + public static void dispatchCompute(int workX, int workY, int workZ) { + GL43.glDispatchCompute(workX, workY, workZ); + } + + public static void dispatchCompute(Vector3i workGroups) { + GL43.glDispatchCompute(workGroups.x, workGroups.y, workGroups.z); + } + + public static void memoryBarrier(int barriers) { + if (supportsCompute) { + GL42.glMemoryBarrier(barriers); + } + } + + public static boolean supportsBufferBlending() { + return Iris.capabilities.GL_ARB_draw_buffers_blend || Iris.capabilities.OpenGL40; + } + + public static void disableBufferBlend(int buffer) { + GL30.glDisablei(GL11.GL_BLEND, buffer); + } + + public static void enableBufferBlend(int buffer) { + GL30.glEnablei(GL11.GL_BLEND, buffer); + } + + public static void blendFuncSeparatei(int buffer, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) { + GL40.glBlendFuncSeparatei(buffer, srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + public static void bindTextureToUnit(int unit, int texture) { + dsaState.bindTextureToUnit(unit, texture); + } + + public static FloatBuffer PROJECTION_MATRIX_BUFFER = BufferUtils.createFloatBuffer(16); + public static void setupProjectionMatrix(Matrix4f matrix) { + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPushMatrix(); + PROJECTION_MATRIX_BUFFER.clear().rewind(); + matrix.get(PROJECTION_MATRIX_BUFFER); + GL11.glLoadMatrix(PROJECTION_MATRIX_BUFFER); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + } + + public static void restoreProjectionMatrix() { + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPopMatrix(); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + } + + public static void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2, int bufferChoice, int filter) { + dsaState.blitFramebuffer(source, dest, offsetX, offsetY, width, height, offsetX2, offsetY2, width2, height2, bufferChoice, filter); + } + + public static int createFramebuffer() { + return dsaState.createFramebuffer(); + } + + public static int createTexture(int target) { + return dsaState.createTexture(target); + } + + public interface DSAAccess { + void generateMipmaps(int texture, int target); + + void texParameteri(int texture, int target, int pname, int param); + void texParameterf(int texture, int target, int pname, float param); + void texParameteriv(int texture, int target, int pname, IntBuffer params); + + void readBuffer(int framebuffer, int buffer); + + void drawBuffers(int framebuffer, IntBuffer buffers); + + int getTexParameteri(int texture, int target, int pname); + + void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height); + + void bindTextureToUnit(int unit, int texture); + + int bufferStorage(int target, FloatBuffer data, int usage); + + void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2, int bufferChoice, int filter); + + void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels); + + int createFramebuffer(); + int createTexture(int target); + } + + public static class DSACore extends DSAARB { + + } + + public static class DSAARB extends DSAUnsupported { + + @Override + public void generateMipmaps(int texture, int target) { + ARBDirectStateAccess.glGenerateTextureMipmap(texture); + } + + @Override + public void texParameteri(int texture, int target, int pname, int param) { + ARBDirectStateAccess.glTextureParameteri(texture, pname, param); + } + + @Override + public void texParameterf(int texture, int target, int pname, float param) { + ARBDirectStateAccess.glTextureParameterf(texture, pname, param); + } + + @Override + public void texParameteriv(int texture, int target, int pname, IntBuffer params) { + ARBDirectStateAccess.glTextureParameter(texture, pname, params); + } + + @Override + public void readBuffer(int framebuffer, int buffer) { + ARBDirectStateAccess.glNamedFramebufferReadBuffer(framebuffer, buffer); + } + + @Override + public void drawBuffers(int framebuffer, IntBuffer buffers) { + ARBDirectStateAccess.glNamedFramebufferDrawBuffers(framebuffer, buffers); + } + + @Override + public int getTexParameteri(int texture, int target, int pname) { + return ARBDirectStateAccess.glGetTextureParameteri(texture, pname); + } + + @Override + public void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height) { + ARBDirectStateAccess.glCopyTextureSubImage2D(destTexture, i, i1, i2, i3, i4, width, height); + } + + @Override + public void bindTextureToUnit(int unit, int texture) { + if (texture == 0) { + super.bindTextureToUnit(unit, texture); + } else { + ARBDirectStateAccess.glBindTextureUnit(unit, texture); + } + } + + @Override + public int bufferStorage(int target, FloatBuffer data, int usage) { + int buffer = GL45.glCreateBuffers(); + GL45.glNamedBufferData(buffer, data, usage); + return buffer; + } + + @Override + public void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2, int bufferChoice, int filter) { + ARBDirectStateAccess.glBlitNamedFramebuffer(source, dest, offsetX, offsetY, width, height, offsetX2, offsetY2, width2, height2, bufferChoice, filter); + } + + @Override + public void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { + ARBDirectStateAccess.glNamedFramebufferTexture(fb, attachment, texture, levels); + } + + @Override + public int createFramebuffer() { + return ARBDirectStateAccess.glCreateFramebuffers(); + } + + @Override + public int createTexture(int target) { + return ARBDirectStateAccess.glCreateTextures(target); + } + } + + public static class DSAUnsupported implements DSAAccess { + @Override + public void generateMipmaps(int texture, int target) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GL30.glGenerateMipmap(target); + } + + @Override + public void texParameteri(int texture, int target, int pname, int param) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GL11.glTexParameteri(target, pname, param); + } + + @Override + public void texParameterf(int texture, int target, int pname, float param) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GL11.glTexParameterf(target, pname, param); + } + + @Override + public void texParameteriv(int texture, int target, int pname, IntBuffer params) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GL11.glTexParameter(target, pname, params); + } + + @Override + public void readBuffer(int framebuffer, int buffer) { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer); + GL11.glReadBuffer(buffer); + } + + @Override + public void drawBuffers(int framebuffer, IntBuffer buffers) { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer); + GL20.glDrawBuffers(buffers); + } + + @Override + public int getTexParameteri(int texture, int target, int pname) { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + return GL11.glGetTexParameteri(target, pname); + } + + @Override + public void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height) { + int previous = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, destTexture); + GL11.glCopyTexSubImage2D(target, i, i1, i2, i3, i4, width, height); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, previous); + } + + @Override + public void bindTextureToUnit(int unit, int texture) { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + unit); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + } + + @Override + public int bufferStorage(int target, FloatBuffer data, int usage) { + int buffer = GL15.glGenBuffers(); + GL15.glBindBuffer(target, buffer); + bufferData(target, data, usage); + GL15.glBindBuffer(target, 0); + + return buffer; + } + + @Override + public void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2, int bufferChoice, int filter) { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, source); + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_DRAW_FRAMEBUFFER, dest); + GL30.glBlitFramebuffer(offsetX, offsetY, width, height, offsetX2, offsetY2, width2, height2, bufferChoice, filter); + } + + @Override + public void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(fbtarget, fb); + GL30.glFramebufferTexture2D(fbtarget, attachment, target, texture, levels); + } + + @Override + public int createFramebuffer() { + int framebuffer = OpenGlHelper.func_153165_e/*glGenFramebuffers*/(); + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer); + return framebuffer; + } + + @Override + public int createTexture(int target) { + int texture = GL11.glGenTextures(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + return texture; + } + } + + /* + public static void bindTextures(int startingTexture, int[] bindings) { + if (hasMultibind) { + ARBMultiBind.glBindTextures(startingTexture, bindings); + } else if (dsaState != DSAState.NONE) { + for (int binding : bindings) { + ARBDirectStateAccess.glBindTextureUnit(startingTexture, binding); + startingTexture++; + } + } else { + for (int binding : bindings) { + GLStateManager.glActiveTexture(startingTexture); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, binding); + startingTexture++; + } + } + } + */ + + // TODO: Proper notification of compute support + public static boolean supportsCompute() { + return Iris.capabilities.GL_ARB_compute_shader; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/AlphaTest.java b/src/main/java/net/coderbot/iris/gl/blending/AlphaTest.java new file mode 100644 index 000000000..93d8ede42 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/AlphaTest.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gl.blending; + +public class AlphaTest { + private final AlphaTestFunction function; + private final float reference; + + public AlphaTest(AlphaTestFunction function, float reference) { + this.function = function; + this.reference = reference; + } + + public AlphaTestFunction getFunction() { + return function; + } + + public float getReference() { + return reference; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/AlphaTestFunction.java b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestFunction.java new file mode 100644 index 000000000..6a15e3d61 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestFunction.java @@ -0,0 +1,57 @@ +package net.coderbot.iris.gl.blending; + +import org.lwjgl.opengl.GL11; + +import java.util.Optional; + +public enum AlphaTestFunction { + NEVER(GL11.GL_NEVER), + LESS(GL11.GL_LESS), + EQUAL(GL11.GL_EQUAL), + LEQUAL(GL11.GL_LEQUAL), + GREATER(GL11.GL_GREATER), + NOTEQUAL(GL11.GL_NOTEQUAL), + GEQUAL(GL11.GL_GEQUAL), + ALWAYS(GL11.GL_ALWAYS); + + private final int glId; + + AlphaTestFunction(int glFormat) { + this.glId = glFormat; + } + + public static Optional fromGlId(int glId) { + return switch (glId) { + case GL11.GL_NEVER -> Optional.of(NEVER); + case GL11.GL_LESS -> Optional.of(LESS); + case GL11.GL_EQUAL -> Optional.of(EQUAL); + case GL11.GL_LEQUAL -> Optional.of(LEQUAL); + case GL11.GL_GREATER -> Optional.of(GREATER); + case GL11.GL_NOTEQUAL -> Optional.of(NOTEQUAL); + case GL11.GL_GEQUAL -> Optional.of(GEQUAL); + case GL11.GL_ALWAYS -> Optional.of(ALWAYS); + default -> Optional.empty(); + }; + } + + public static Optional fromString(String name) { + if ("GL_ALWAYS".equals(name)) { + // shaders.properties states that GL_ALWAYS is the name to use, but I haven't verified that this actually + // matches the implementation... All of the other names do not have the GL_ prefix. + // + // We'll support it here just to be safe, even though just a plain ALWAYS seems more likely to be what it + // parses. + return Optional.of(AlphaTestFunction.ALWAYS); + } + + try { + return Optional.of(AlphaTestFunction.valueOf(name)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + public int getGlId() { + return glId; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/AlphaTestOverride.java b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestOverride.java new file mode 100644 index 000000000..3df5d5907 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestOverride.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gl.blending; + +public class AlphaTestOverride { + public static final AlphaTestOverride OFF = new AlphaTestOverride(null); + + private final AlphaTest alphaTest; + + public AlphaTestOverride(AlphaTest alphaTest) { + this.alphaTest = alphaTest; + } + + public void apply() { + AlphaTestStorage.overrideAlphaTest(this.alphaTest); + } + + public static void restore() { + AlphaTestStorage.restoreAlphaTest(); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/AlphaTestStorage.java b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestStorage.java new file mode 100644 index 000000000..f8ef2ba67 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/AlphaTestStorage.java @@ -0,0 +1,57 @@ +package net.coderbot.iris.gl.blending; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.AlphaState; +import lombok.Getter; + +public class AlphaTestStorage { + private static boolean originalAlphaTestEnable; + private static AlphaTest originalAlphaTest; + @Getter + private static boolean alphaTestLocked; + + public static void overrideAlphaTest(AlphaTest override) { + if (!alphaTestLocked) { + final AlphaState alphaState = GLStateManager.getAlphaState(); + + // Only save the previous state if the alpha test wasn't already locked + originalAlphaTestEnable = alphaState.mode.isEnabled(); + originalAlphaTest = new AlphaTest(AlphaTestFunction.fromGlId(alphaState.function).orElse(AlphaTestFunction.ALWAYS), alphaState.reference); + } + + alphaTestLocked = false; + + if (override == null) { + GLStateManager.disableAlphaTest(); + } else { + GLStateManager.enableAlphaTest(); + GLStateManager.glAlphaFunc(override.getFunction().getGlId(), override.getReference()); + } + + alphaTestLocked = true; + } + + public static void deferAlphaTestToggle(boolean enabled) { + originalAlphaTestEnable = enabled; + } + + public static void deferAlphaFunc(int function, float reference) { + originalAlphaTest = new AlphaTest(AlphaTestFunction.fromGlId(function).get(), reference); + } + + public static void restoreAlphaTest() { + if (!alphaTestLocked) { + return; + } + + alphaTestLocked = false; + + if (originalAlphaTestEnable) { + GLStateManager.enableAlphaTest(); + } else { + GLStateManager.disableAlphaTest(); + } + + GLStateManager.glAlphaFunc(originalAlphaTest.getFunction().getGlId(), originalAlphaTest.getReference()); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BlendMode.java b/src/main/java/net/coderbot/iris/gl/blending/BlendMode.java new file mode 100644 index 000000000..659cec779 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BlendMode.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.gl.blending; + +public class BlendMode { + private final int srcRgb; + private final int dstRgb; + private final int srcAlpha; + private final int dstAlpha; + + public BlendMode(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + this.srcRgb = srcRgb; + this.dstRgb = dstRgb; + this.srcAlpha = srcAlpha; + this.dstAlpha = dstAlpha; + } + + public int getSrcRgb() { + return srcRgb; + } + + public int getDstRgb() { + return dstRgb; + } + + public int getSrcAlpha() { + return srcAlpha; + } + + public int getDstAlpha() { + return dstAlpha; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BlendModeFunction.java b/src/main/java/net/coderbot/iris/gl/blending/BlendModeFunction.java new file mode 100644 index 000000000..44ab9b141 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BlendModeFunction.java @@ -0,0 +1,39 @@ +package net.coderbot.iris.gl.blending; + +import net.coderbot.iris.Iris; +import org.lwjgl.opengl.GL11; + +import java.util.Optional; + +public enum BlendModeFunction { + ZERO(GL11.GL_ZERO), + ONE(GL11.GL_ONE), + SRC_COLOR(GL11.GL_SRC_COLOR), + ONE_MINUS_SRC_COLOR(GL11.GL_ONE_MINUS_SRC_COLOR), + DST_COLOR(GL11.GL_DST_COLOR), + ONE_MINUS_DST_COLOR(GL11.GL_ONE_MINUS_DST_COLOR), + SRC_ALPHA(GL11.GL_SRC_ALPHA), + ONE_MINUS_SRC_ALPHA(GL11.GL_ONE_MINUS_SRC_ALPHA), + DST_ALPHA(GL11.GL_DST_ALPHA), + ONE_MINUS_DST_ALPHA(GL11.GL_ONE_MINUS_DST_ALPHA), + SRC_ALPHA_SATURATE(GL11.GL_SRC_ALPHA_SATURATE); + + private final int glId; + + BlendModeFunction(int glFormat) { + this.glId = glFormat; + } + + public static Optional fromString(String name) { + try { + return Optional.of(BlendModeFunction.valueOf(name)); + } catch (IllegalArgumentException e) { + Iris.logger.warn("Invalid blend mode! " + name); + return Optional.empty(); + } + } + + public int getGlId() { + return glId; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BlendModeOverride.java b/src/main/java/net/coderbot/iris/gl/blending/BlendModeOverride.java new file mode 100644 index 000000000..ab8de89a1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BlendModeOverride.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gl.blending; + +public class BlendModeOverride { + public static final BlendModeOverride OFF = new BlendModeOverride(null); + + private final BlendMode blendMode; + + public BlendModeOverride(BlendMode blendMode) { + this.blendMode = blendMode; + } + + public void apply() { + BlendModeStorage.overrideBlend(this.blendMode); + } + + public static void restore() { + BlendModeStorage.restoreBlend(); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java b/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java new file mode 100644 index 000000000..67682a206 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java @@ -0,0 +1,76 @@ +package net.coderbot.iris.gl.blending; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL14; + +public class BlendModeStorage { + private static boolean originalBlendEnable; + private static BlendMode originalBlend; + private static boolean blendLocked; + + public static boolean isBlendLocked() { + return blendLocked; + } + + public static void overrideBlend(BlendMode override) { + if (!blendLocked) { + // Only save the previous state if the blend mode wasn't already locked + originalBlendEnable = GL11.glGetBoolean(GL11.GL_BLEND); + originalBlend = new BlendMode(GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB), GL11.glGetInteger(GL14.GL_BLEND_DST_RGB), GL11.glGetInteger(GL14.GL_BLEND_SRC_ALPHA), GL11.glGetInteger(GL14.GL_BLEND_DST_ALPHA)); + } + + blendLocked = false; + + if (override == null) { + GL11.glDisable(GL11.GL_BLEND); + } else { + GL11.glEnable(GL11.GL_BLEND); + + GL14.glBlendFuncSeparate(override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha()); + } + + blendLocked = true; + } + + public static void overrideBufferBlend(int index, BlendMode override) { + if (!blendLocked) { + // Only save the previous state if the blend mode wasn't already locked + originalBlendEnable = GL11.glGetBoolean(GL11.GL_BLEND); + originalBlend = new BlendMode(GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB), GL11.glGetInteger(GL14.GL_BLEND_DST_RGB), GL11.glGetInteger(GL14.GL_BLEND_SRC_ALPHA), GL11.glGetInteger(GL14.GL_BLEND_DST_ALPHA)); + } + + if (override == null) { + IrisRenderSystem.disableBufferBlend(index); + } else { + IrisRenderSystem.enableBufferBlend(index); + IrisRenderSystem.blendFuncSeparatei(index, override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha()); + } + + blendLocked = true; + } + + public static void deferBlendModeToggle(boolean enabled) { + originalBlendEnable = enabled; + } + + public static void deferBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + originalBlend = new BlendMode(srcRgb, dstRgb, srcAlpha, dstAlpha); + } + + public static void restoreBlend() { + if (!blendLocked) { + return; + } + + blendLocked = false; + + if (originalBlendEnable) { + GL11.glEnable(GL11.GL_BLEND); + } else { + GL11.glDisable(GL11.GL_BLEND); + } + + GL14.glBlendFuncSeparate(originalBlend.getSrcRgb(), originalBlend.getDstRgb(), originalBlend.getSrcAlpha(), originalBlend.getDstAlpha()); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BufferBlendInformation.java b/src/main/java/net/coderbot/iris/gl/blending/BufferBlendInformation.java new file mode 100644 index 000000000..bbd688a27 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BufferBlendInformation.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gl.blending; + +public class BufferBlendInformation { + private final int index; + private final BlendMode blendMode; + + public BufferBlendInformation(int index, BlendMode blendMode) { + this.index = index; + this.blendMode = blendMode; + } + + public BlendMode getBlendMode() { + return blendMode; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/BufferBlendOverride.java b/src/main/java/net/coderbot/iris/gl/blending/BufferBlendOverride.java new file mode 100644 index 000000000..ca3fbb801 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/BufferBlendOverride.java @@ -0,0 +1,15 @@ +package net.coderbot.iris.gl.blending; + +public class BufferBlendOverride { + private final int drawBuffer; + private final BlendMode blendMode; + + public BufferBlendOverride(int drawBuffer, BlendMode blendMode) { + this.drawBuffer = drawBuffer; + this.blendMode = blendMode; + } + + public void apply() { + BlendModeStorage.overrideBufferBlend(this.drawBuffer, this.blendMode); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/ColorMask.java b/src/main/java/net/coderbot/iris/gl/blending/ColorMask.java new file mode 100644 index 000000000..73e6a174b --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/ColorMask.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.gl.blending; + +public class ColorMask { + private final boolean red; + private final boolean green; + private final boolean blue; + private final boolean alpha; + + public ColorMask(boolean red, boolean green, boolean blue, boolean alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + + public boolean isRedMasked() { + return red; + } + + public boolean isGreenMasked() { + return green; + } + + public boolean isBlueMasked() { + return blue; + } + + public boolean isAlphaMasked() { + return alpha; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/blending/DepthColorStorage.java b/src/main/java/net/coderbot/iris/gl/blending/DepthColorStorage.java new file mode 100644 index 000000000..70086ef3f --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/blending/DepthColorStorage.java @@ -0,0 +1,51 @@ +package net.coderbot.iris.gl.blending; + +import com.gtnewhorizons.angelica.glsm.states.DepthState; +import com.gtnewhorizons.angelica.glsm.states.GLColorMask; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; + +public class DepthColorStorage { + private static boolean originalDepthEnable; + private static ColorMask originalColor; + @Getter + private static boolean depthColorLocked; + + public static void disableDepthColor() { + if (!depthColorLocked) { + // Only save the previous state if the depth and color mask wasn't already locked + GLColorMask colorMask = GLStateManager.getColorMask(); + final DepthState depthState = GLStateManager.getDepthState(); + + originalDepthEnable = depthState.mask; + originalColor = new ColorMask(colorMask.red, colorMask.green, colorMask.blue, colorMask.alpha); + } + + depthColorLocked = false; + + GLStateManager.glDepthMask(false); + GLStateManager.glColorMask(false, false, false, false); + + depthColorLocked = true; + } + + public static void deferDepthEnable(boolean enabled) { + originalDepthEnable = enabled; + } + + public static void deferColorMask(boolean red, boolean green, boolean blue, boolean alpha) { + originalColor = new ColorMask(red, green, blue, alpha); + } + + public static void unlockDepthColor() { + if (!depthColorLocked) { + return; + } + + depthColorLocked = false; + + GLStateManager.glDepthMask(originalDepthEnable); + + GLStateManager.glColorMask(originalColor.isRedMasked(), originalColor.isGreenMasked(), originalColor.isBlueMasked(), originalColor.isAlphaMasked()); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/framebuffer/GlFramebuffer.java b/src/main/java/net/coderbot/iris/gl/framebuffer/GlFramebuffer.java new file mode 100644 index 000000000..2f85ba66d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/framebuffer/GlFramebuffer.java @@ -0,0 +1,113 @@ +package net.coderbot.iris.gl.framebuffer; + +import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.DepthBufferFormat; +import net.coderbot.iris.texture.TextureInfoCache; +import net.minecraft.client.renderer.OpenGlHelper; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; + +import java.nio.IntBuffer; + +public class GlFramebuffer extends GlResource { + private final Int2IntMap attachments; + private final int maxDrawBuffers; + private final int maxColorAttachments; + private boolean hasDepthAttachment; + + public GlFramebuffer() { + super(IrisRenderSystem.createFramebuffer()); + + this.attachments = new Int2IntArrayMap(); + this.maxDrawBuffers = GL11.glGetInteger(GL20.GL_MAX_DRAW_BUFFERS); + this.maxColorAttachments = GL11.glGetInteger(GL30.GL_MAX_COLOR_ATTACHMENTS); + this.hasDepthAttachment = false; + } + + public void addDepthAttachment(int texture) { + int internalFormat = TextureInfoCache.INSTANCE.getInfo(texture).getInternalFormat(); + DepthBufferFormat depthBufferFormat = DepthBufferFormat.fromGlEnumOrDefault(internalFormat); + + int fb = getGlId(); + + if (depthBufferFormat.isCombinedStencil()) { + IrisRenderSystem.framebufferTexture2D(fb, GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_STENCIL_ATTACHMENT, GL11.GL_TEXTURE_2D, texture, 0); + } else { + IrisRenderSystem.framebufferTexture2D(fb, GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL11.GL_TEXTURE_2D, texture, 0); + } + + this.hasDepthAttachment = true; + } + + public void addColorAttachment(int index, int texture) { + int fb = getGlId(); + + IrisRenderSystem.framebufferTexture2D(fb, GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0 + index, GL11.GL_TEXTURE_2D, texture, 0); + attachments.put(index, texture); + } + + public void noDrawBuffers() { + IntBuffer buffer = BufferUtils.createIntBuffer(1); + buffer.put(GL11.GL_NONE); + IrisRenderSystem.drawBuffers(getGlId(), buffer); + } + + public void drawBuffers(int[] buffers) { + IntBuffer glBuffers = BufferUtils.createIntBuffer(buffers.length); + int index = 0; + + if (buffers.length > maxDrawBuffers) { + throw new IllegalArgumentException("Cannot write to more than " + maxDrawBuffers + " draw buffers on this GPU"); + } + for (int buffer : buffers) { + if (buffer >= maxColorAttachments) { + throw new IllegalArgumentException("Only " + maxColorAttachments + " color attachments are supported on this GPU, but an attempt was made to write to a color attachment with index " + buffer); + } + glBuffers.put(index++, GL30.GL_COLOR_ATTACHMENT0 + buffer); + } + IrisRenderSystem.drawBuffers(getGlId(), glBuffers); + } + + public void readBuffer(int buffer) { + IrisRenderSystem.readBuffer(getGlId(), GL30.GL_COLOR_ATTACHMENT0 + buffer); + } + + public int getColorAttachment(int index) { + return attachments.get(index); + } + + public boolean hasDepthAttachment() { + return hasDepthAttachment; + } + + public void bind() { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, getGlId()); + } + + public void bindAsReadBuffer() { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, getGlId()); + } + + public void bindAsDrawBuffer() { + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_DRAW_FRAMEBUFFER, getGlId()); + } + + protected void destroyInternal() { + OpenGlHelper.func_153184_g/*glDeleteFramebuffers*/(getGlId()); + } + + public boolean isComplete() { + bind(); + + return OpenGlHelper.func_153167_i/*glCheckFramebufferStatus*/(GL30.GL_FRAMEBUFFER) == GL30.GL_FRAMEBUFFER_COMPLETE; + } + + public int getId() { + return getGlId(); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/image/ImageBinding.java b/src/main/java/net/coderbot/iris/gl/image/ImageBinding.java new file mode 100644 index 000000000..6d66099bd --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/image/ImageBinding.java @@ -0,0 +1,24 @@ +package net.coderbot.iris.gl.image; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.lwjgl.opengl.GL15; + +import java.util.function.IntSupplier; + +public class ImageBinding { + private final int imageUnit; + private final int internalFormat; + private final IntSupplier textureID; + + public ImageBinding(int imageUnit, int internalFormat, IntSupplier textureID) { + this.textureID = textureID; + this.imageUnit = imageUnit; + this.internalFormat = internalFormat; + } + + public void update() { + // We can assume that image bindings are supported here as either the EXT extension or 4.2 core, as otherwise ImageLimits + // would report that zero image units are supported. + IrisRenderSystem.bindImageTexture(imageUnit, textureID.getAsInt(), 0, false, 0, GL15.GL_READ_WRITE, internalFormat); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/image/ImageHolder.java b/src/main/java/net/coderbot/iris/gl/image/ImageHolder.java new file mode 100644 index 000000000..9b9f66d8a --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/image/ImageHolder.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.gl.image; + +import net.coderbot.iris.gl.texture.InternalTextureFormat; + +import java.util.function.IntSupplier; + +public interface ImageHolder { + boolean hasImage(String name); + void addTextureImage(IntSupplier textureID, InternalTextureFormat internalFormat, String name); +} diff --git a/src/main/java/net/coderbot/iris/gl/image/ImageLimits.java b/src/main/java/net/coderbot/iris/gl/image/ImageLimits.java new file mode 100644 index 000000000..64e784c57 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/image/ImageLimits.java @@ -0,0 +1,24 @@ +package net.coderbot.iris.gl.image; + +import net.coderbot.iris.gl.IrisRenderSystem; + +public class ImageLimits { + private final int maxImageUnits; + private static ImageLimits instance; + + private ImageLimits() { + this.maxImageUnits = IrisRenderSystem.getMaxImageUnits(); + } + + public int getMaxImageUnits() { + return maxImageUnits; + } + + public static ImageLimits get() { + if (instance == null) { + instance = new ImageLimits(); + } + + return instance; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java b/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java new file mode 100644 index 000000000..3018e643d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java @@ -0,0 +1,95 @@ +package net.coderbot.iris.gl.program; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import org.joml.Vector2f; +import org.joml.Vector3i; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL43; + +import java.nio.IntBuffer; + +public final class ComputeProgram extends GlResource { + private final ProgramUniforms uniforms; + private final ProgramSamplers samplers; + private final ProgramImages images; + private Vector3i absoluteWorkGroups; + private Vector2f relativeWorkGroups; +// private int[] localSize; + private IntBuffer localSizeBuffer; + private float cachedWidth; + private float cachedHeight; + private Vector3i cachedWorkGroups; + + ComputeProgram(int program, ProgramUniforms uniforms, ProgramSamplers samplers, ProgramImages images) { + super(program); + + localSizeBuffer = BufferUtils.createIntBuffer(3); +// localSize = new int[3]; + IrisRenderSystem.getProgramiv(program, GL43.GL_COMPUTE_WORK_GROUP_SIZE, localSizeBuffer); + this.uniforms = uniforms; + this.samplers = samplers; + this.images = images; + } + + public void setWorkGroupInfo(Vector2f relativeWorkGroups, Vector3i absoluteWorkGroups) { + this.relativeWorkGroups = relativeWorkGroups; + this.absoluteWorkGroups = absoluteWorkGroups; + } + + public Vector3i getWorkGroups(float width, float height) { + if (cachedWidth != width || cachedHeight != height || cachedWorkGroups == null) { + this.cachedWidth = width; + this.cachedHeight = height; + if (this.absoluteWorkGroups != null) { + this.cachedWorkGroups = this.absoluteWorkGroups; + } else if (relativeWorkGroups != null) { + // TODO: This is my best guess at what Optifine does. Can this be confirmed? + // Do not use actual localSize here, apparently that's not what we want. + this.cachedWorkGroups = new Vector3i((int) Math.ceil(Math.ceil((width * relativeWorkGroups.x)) / localSizeBuffer.get(0)), (int) Math.ceil(Math.ceil((height * relativeWorkGroups.y)) / localSizeBuffer.get(1)), 1); + } else { + this.cachedWorkGroups = new Vector3i((int) Math.ceil(width / localSizeBuffer.get(0)), (int) Math.ceil(height / localSizeBuffer.get(1)), 1); + } + } + + return cachedWorkGroups; + } + + public void dispatch(float width, float height) { + GL20.glUseProgram(getGlId()); + uniforms.update(); + samplers.update(); + images.update(); + + if (!Iris.getPipelineManager().getPipeline().map(WorldRenderingPipeline::allowConcurrentCompute).orElse(false)) { + IrisRenderSystem.memoryBarrier(40); + } + + IrisRenderSystem.dispatchCompute(getWorkGroups(width, height)); + } + + public static void unbind() { + ProgramUniforms.clearActiveUniforms(); + GL20.glUseProgram(0); + } + + public void destroyInternal() { + GL20.glDeleteProgram(getGlId()); + } + + /** + * @return the OpenGL ID of this program. + * @deprecated this should be encapsulated eventually + */ + @Deprecated + public int getProgramId() { + return getGlId(); + } + + public int getActiveImages() { + return images.getActiveImages(); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/GlUniform1iCall.java b/src/main/java/net/coderbot/iris/gl/program/GlUniform1iCall.java new file mode 100644 index 000000000..004fc87b3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/GlUniform1iCall.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gl.program; + +public class GlUniform1iCall { + private final int location; + private final int value; + + public GlUniform1iCall(int location, int value) { + this.location = location; + this.value = value; + } + + public int getLocation() { + return location; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/Program.java b/src/main/java/net/coderbot/iris/gl/program/Program.java new file mode 100644 index 000000000..6aa5d6284 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/Program.java @@ -0,0 +1,49 @@ +package net.coderbot.iris.gl.program; + +import net.coderbot.iris.gl.GlResource; +import org.lwjgl.opengl.GL20; + +public final class Program extends GlResource { + private final ProgramUniforms uniforms; + private final ProgramSamplers samplers; + private final ProgramImages images; + + Program(int program, ProgramUniforms uniforms, ProgramSamplers samplers, ProgramImages images) { + super(program); + + this.uniforms = uniforms; + this.samplers = samplers; + this.images = images; + } + + public void use() { + GL20.glUseProgram(getGlId()); + + uniforms.update(); + samplers.update(); + images.update(); + } + + public static void unbind() { + ProgramUniforms.clearActiveUniforms(); + ProgramSamplers.clearActiveSamplers(); + GL20.glUseProgram(0); + } + + public void destroyInternal() { + GL20.glDeleteProgram(getGlId()); + } + + /** + * @return the OpenGL ID of this program. + * @deprecated this should be encapsulated eventually + */ + @Deprecated + public int getProgramId() { + return getGlId(); + } + + public int getActiveImages() { + return images.getActiveImages(); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/ProgramBuilder.java b/src/main/java/net/coderbot/iris/gl/program/ProgramBuilder.java new file mode 100644 index 000000000..a57483656 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/ProgramBuilder.java @@ -0,0 +1,131 @@ +package net.coderbot.iris.gl.program; + +import com.google.common.collect.ImmutableSet; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.image.ImageHolder; +import net.coderbot.iris.gl.sampler.SamplerHolder; +import net.coderbot.iris.gl.shader.GlShader; +import net.coderbot.iris.gl.shader.ProgramCreator; +import net.coderbot.iris.gl.shader.ShaderType; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import org.jetbrains.annotations.Nullable; + +import java.util.function.IntSupplier; + +public class ProgramBuilder extends ProgramUniforms.Builder implements SamplerHolder, ImageHolder { + private final int program; + private final ProgramSamplers.Builder samplers; + private final ProgramImages.Builder images; + + private ProgramBuilder(String name, int program, ImmutableSet reservedTextureUnits) { + super(name, program); + + this.program = program; + this.samplers = ProgramSamplers.builder(program, reservedTextureUnits); + this.images = ProgramImages.builder(program); + } + + public void bindAttributeLocation(int index, String name) { + IrisRenderSystem.bindAttributeLocation(program, index, name); + } + + public static ProgramBuilder begin(String name, @Nullable String vertexSource, @Nullable String geometrySource, + @Nullable String fragmentSource, ImmutableSet reservedTextureUnits) { + GlShader vertex; + GlShader geometry; + GlShader fragment; + + vertex = buildShader(ShaderType.VERTEX, name + ".vsh", vertexSource); + + if (geometrySource != null) { + geometry = buildShader(ShaderType.GEOMETRY, name + ".gsh", geometrySource); + } else { + geometry = null; + } + + fragment = buildShader(ShaderType.FRAGMENT, name + ".fsh", fragmentSource); + + int programId; + + if (geometry != null) { + programId = ProgramCreator.create(name, vertex, geometry, fragment); + } else { + programId = ProgramCreator.create(name, vertex, fragment); + } + + vertex.destroy(); + + if (geometry != null) { + geometry.destroy(); + } + + fragment.destroy(); + + return new ProgramBuilder(name, programId, reservedTextureUnits); + } + + public static ProgramBuilder beginCompute(String name, @Nullable String source, ImmutableSet reservedTextureUnits) { + if (!IrisRenderSystem.supportsCompute()) { + throw new IllegalStateException("This PC does not support compute shaders, but it's attempting to be used???"); + } + + GlShader compute = buildShader(ShaderType.COMPUTE, name + ".csh", source); + + int programId = ProgramCreator.create(name, compute); + + compute.destroy(); + + return new ProgramBuilder(name, programId, reservedTextureUnits); + } + + public Program build() { + return new Program(program, super.buildUniforms(), this.samplers.build(), this.images.build()); + } + + public ComputeProgram buildCompute() { + return new ComputeProgram(program, super.buildUniforms(), this.samplers.build(), this.images.build()); + } + + private static GlShader buildShader(ShaderType shaderType, String name, @Nullable String source) { + try { + return new GlShader(shaderType, name, source); + } catch (RuntimeException e) { + throw new RuntimeException("Failed to compile " + shaderType + " shader for program " + name, e); + } + } + + @Override + public void addExternalSampler(int textureUnit, String... names) { + samplers.addExternalSampler(textureUnit, names); + } + + @Override + public boolean hasSampler(String name) { + return samplers.hasSampler(name); + } + + @Override + public boolean addDefaultSampler(IntSupplier sampler, String... names) { + return samplers.addDefaultSampler(sampler, names); + } + + @Override + public boolean addDynamicSampler(IntSupplier sampler, String... names) { + return samplers.addDynamicSampler(sampler, names); + } + + public boolean addDynamicSampler(IntSupplier sampler, ValueUpdateNotifier notifier, String... names) { + return samplers.addDynamicSampler(sampler, notifier, names); + } + + @Override + public boolean hasImage(String name) { + return images.hasImage(name); + } + + @Override + public void addTextureImage(IntSupplier textureID, InternalTextureFormat internalFormat, String name) { + images.addTextureImage(textureID, internalFormat, name); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/ProgramImages.java b/src/main/java/net/coderbot/iris/gl/program/ProgramImages.java new file mode 100644 index 000000000..014ebd7f9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/ProgramImages.java @@ -0,0 +1,99 @@ +package net.coderbot.iris.gl.program; + +import com.google.common.collect.ImmutableList; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.image.ImageBinding; +import net.coderbot.iris.gl.image.ImageHolder; +import net.coderbot.iris.gl.image.ImageLimits; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import org.lwjgl.opengl.GL20; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntSupplier; + +public class ProgramImages { + private final ImmutableList imageBindings; + private List initializer; + + private ProgramImages(ImmutableList imageBindings, List initializer) { + this.imageBindings = imageBindings; + this.initializer = initializer; + } + + public void update() { + if (initializer != null) { + for (GlUniform1iCall call : initializer) { + IrisRenderSystem.uniform1i(call.getLocation(), call.getValue()); + } + + initializer = null; + } + + for (ImageBinding imageBinding : imageBindings) { + imageBinding.update(); + } + } + + public int getActiveImages() { + return imageBindings.size(); + } + + public static Builder builder(int program) { + return new Builder(program); + } + + public static final class Builder implements ImageHolder { + private final int program; + private final ImmutableList.Builder images; + private final List calls; + private int nextImageUnit; + private final int maxImageUnits; + + private Builder(int program) { + this.program = program; + this.images = ImmutableList.builder(); + this.calls = new ArrayList<>(); + this.nextImageUnit = 0; + this.maxImageUnits = ImageLimits.get().getMaxImageUnits(); + } + + @Override + public boolean hasImage(String name) { + return GL20.glGetUniformLocation(program, name) != -1; + } + + @Override + public void addTextureImage(IntSupplier textureID, InternalTextureFormat internalFormat, String name) { + int location = GL20.glGetUniformLocation(program, name); + + if (location == -1) { + return; + } + + if (nextImageUnit >= maxImageUnits) { + if (maxImageUnits == 0) { + throw new IllegalStateException("Image units are not supported on this platform, but a shader" + + " program attempted to reference " + name + "."); + } else { + throw new IllegalStateException("No more available texture units while activating image " + name + "." + + " Only " + maxImageUnits + " image units are available."); + } + } + + if (internalFormat == InternalTextureFormat.RGBA) { + // Internal detail of Optifine: Set RGBA8 if RGBA is selected, as RGBA is not valid for images. + internalFormat = InternalTextureFormat.RGBA8; + } + + images.add(new ImageBinding(nextImageUnit, internalFormat.getGlFormat(), textureID)); + calls.add(new GlUniform1iCall(location, nextImageUnit)); + + nextImageUnit += 1; + } + + public ProgramImages build() { + return new ProgramImages(images.build(), calls); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/ProgramSamplers.java b/src/main/java/net/coderbot/iris/gl/program/ProgramSamplers.java new file mode 100644 index 000000000..e17c8b94f --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/ProgramSamplers.java @@ -0,0 +1,295 @@ +package net.coderbot.iris.gl.program; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.sampler.SamplerBinding; +import net.coderbot.iris.gl.sampler.SamplerHolder; +import net.coderbot.iris.gl.sampler.SamplerLimits; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import org.lwjgl.opengl.GL13; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.IntSupplier; + +public class ProgramSamplers { + private static ProgramSamplers active; + private final ImmutableList samplerBindings; + private final ImmutableList notifiersToReset; + private List initializer; + + private ProgramSamplers(ImmutableList samplerBindings, ImmutableList notifiersToReset, List initializer) { + this.samplerBindings = samplerBindings; + this.notifiersToReset = notifiersToReset; + this.initializer = initializer; + } + + public void update() { + if (active != null) { + active.removeListeners(); + } + + active = this; + + if (initializer != null) { + for (GlUniform1iCall call : initializer) { + IrisRenderSystem.uniform1i(call.getLocation(), call.getValue()); + } + + initializer = null; + } + + // We need to keep the active texture intact, since if we mess it up + // in the middle of RenderType setup, bad things will happen. + int activeTexture = GLStateManager.getActiveTexture(); + + for (SamplerBinding samplerBinding : samplerBindings) { + samplerBinding.update(); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + activeTexture); + } + + public void removeListeners() { + active = null; + + for (ValueUpdateNotifier notifier : notifiersToReset) { + notifier.setListener(null); + } + } + + public static void clearActiveSamplers() { + if (active != null) { + active.removeListeners(); + } + } + + public static Builder builder(int program, Set reservedTextureUnits) { + return new Builder(program, reservedTextureUnits); + } + + public static CustomTextureSamplerInterceptor customTextureSamplerInterceptor(SamplerHolder samplerHolder, Object2ObjectMap customTextureIds) { + return customTextureSamplerInterceptor(samplerHolder, customTextureIds, ImmutableSet.of()); + } + + public static CustomTextureSamplerInterceptor customTextureSamplerInterceptor(SamplerHolder samplerHolder, Object2ObjectMap customTextureIds, ImmutableSet flippedAtLeastOnceSnapshot) { + return new CustomTextureSamplerInterceptor(samplerHolder, customTextureIds, flippedAtLeastOnceSnapshot); + } + + public static final class Builder implements SamplerHolder { + private final int program; + private final ImmutableSet reservedTextureUnits; + private final ImmutableList.Builder samplers; + private final ImmutableList.Builder notifiersToReset; + private final List calls; + private int remainingUnits; + private int nextUnit; + + private Builder(int program, Set reservedTextureUnits) { + this.program = program; + this.reservedTextureUnits = ImmutableSet.copyOf(reservedTextureUnits); + this.samplers = ImmutableList.builder(); + this.notifiersToReset = ImmutableList.builder(); + this.calls = new ArrayList<>(); + + int maxTextureUnits = SamplerLimits.get().getMaxTextureUnits(); + + for (int unit : reservedTextureUnits) { + if (unit >= maxTextureUnits) { + throw new IllegalStateException("Cannot mark texture unit " + unit + " as reserved because that " + + "texture unit isn't available on this system! Only " + maxTextureUnits + + " texture units are available."); + } + } + + this.remainingUnits = maxTextureUnits - reservedTextureUnits.size(); + this.nextUnit = 0; + + while (reservedTextureUnits.contains(nextUnit)) { + nextUnit += 1; + } + + //System.out.println("Begin building samplers. Reserved texture units are " + reservedTextureUnits + + // ", next texture unit is " + nextUnit + ", there are " + remainingUnits + " units remaining."); + } + + @Override + public void addExternalSampler(int textureUnit, String... names) { + if (!reservedTextureUnits.contains(textureUnit)) { + throw new IllegalArgumentException("Cannot add an externally-managed sampler for texture unit " + + textureUnit + " since it isn't in the set of reserved texture units."); + } + + for (String name : names) { + int location = IrisRenderSystem.getUniformLocation(program, name); + + if (location == -1) { + // There's no active sampler with this particular name in the program. + continue; + } + + // Set up this sampler uniform to use this particular texture unit. + //System.out.println("Binding external sampler " + name + " to texture unit " + textureUnit); + calls.add(new GlUniform1iCall(location, textureUnit)); + } + } + + @Override + public boolean hasSampler(String name) { + return IrisRenderSystem.getUniformLocation(program, name) != -1; + } + + @Override + public boolean addDefaultSampler(IntSupplier sampler, String... names) { + if (nextUnit != 0) { + // TODO: Relax this restriction! + throw new IllegalStateException("Texture unit 0 is already used."); + } + + return addDynamicSampler(sampler, true, null, names); + } + + /** + * Adds a sampler + * @return false if this sampler is not active, true if at least one of the names referred to an active sampler + */ + @Override + public boolean addDynamicSampler(IntSupplier sampler, String... names) { + return addDynamicSampler(sampler, false, null, names); + } + + /** + * Adds a sampler + * @return false if this sampler is not active, true if at least one of the names referred to an active sampler + */ + @Override + public boolean addDynamicSampler(IntSupplier sampler, ValueUpdateNotifier notifier, String... names) { + notifiersToReset.add(notifier); + return addDynamicSampler(sampler, false, notifier, names); + } + + private boolean addDynamicSampler(IntSupplier sampler, boolean used, ValueUpdateNotifier notifier, String... names) { + for (String name : names) { + int location = IrisRenderSystem.getUniformLocation(program, name); + + if (location == -1) { + // There's no active sampler with this particular name in the program. + continue; + } + + // Make sure that we aren't out of texture units. + if (remainingUnits <= 0) { + throw new IllegalStateException("No more available texture units while activating sampler " + name); + } + + //System.out.println("Binding dynamic sampler " + name + " to texture unit " + nextUnit); + + // Set up this sampler uniform to use this particular texture unit. + calls.add(new GlUniform1iCall(location, nextUnit)); + + // And mark this texture unit as used. + used = true; + } + + if (!used) { + return false; + } + + samplers.add(new SamplerBinding(nextUnit, sampler, notifier)); + + remainingUnits -= 1; + nextUnit += 1; + + while (remainingUnits > 0 && reservedTextureUnits.contains(nextUnit)) { + nextUnit += 1; + } + + //System.out.println("The next unit is " + nextUnit + ", there are " + remainingUnits + " units remaining."); + + return true; + } + + public ProgramSamplers build() { + return new ProgramSamplers(samplers.build(), notifiersToReset.build(), calls); + } + } + + public static final class CustomTextureSamplerInterceptor implements SamplerHolder { + private final SamplerHolder samplerHolder; + private final Object2ObjectMap customTextureIds; + private final ImmutableSet deactivatedOverrides; + + private CustomTextureSamplerInterceptor(SamplerHolder samplerHolder, Object2ObjectMap customTextureIds, ImmutableSet flippedAtLeastOnceSnapshot) { + this.samplerHolder = samplerHolder; + this.customTextureIds = customTextureIds; + + ImmutableSet.Builder deactivatedOverrides = new ImmutableSet.Builder<>(); + + for (int deactivatedOverride : flippedAtLeastOnceSnapshot) { + deactivatedOverrides.add("colortex" + deactivatedOverride); + + if (deactivatedOverride < PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.size()) { + deactivatedOverrides.add(PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.get(deactivatedOverride)); + } + } + + this.deactivatedOverrides = deactivatedOverrides.build(); + } + + private IntSupplier getOverride(IntSupplier existing, String... names) { + for (String name : names) { + if (customTextureIds.containsKey(name) && !deactivatedOverrides.contains(name)) { + return customTextureIds.get(name); + } + } + + return existing; + } + + @Override + public void addExternalSampler(int textureUnit, String... names) { + IntSupplier override = getOverride(null, names); + + if (override != null) { + if (textureUnit == 0) { + samplerHolder.addDefaultSampler(override, names); + } else { + samplerHolder.addDynamicSampler(override, names); + } + } else { + samplerHolder.addExternalSampler(textureUnit, names); + } + } + + @Override + public boolean hasSampler(String name) { + return samplerHolder.hasSampler(name); + } + + @Override + public boolean addDefaultSampler(IntSupplier sampler, String... names) { + sampler = getOverride(sampler, names); + + return samplerHolder.addDefaultSampler(sampler, names); + } + + @Override + public boolean addDynamicSampler(IntSupplier sampler, String... names) { + sampler = getOverride(sampler, names); + + return samplerHolder.addDynamicSampler(sampler, names); + } + + @Override + public boolean addDynamicSampler(IntSupplier sampler, ValueUpdateNotifier notifier, String... names) { + sampler = getOverride(sampler, names); + + return samplerHolder.addDynamicSampler(sampler, notifier, names); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/program/ProgramUniforms.java b/src/main/java/net/coderbot/iris/gl/program/ProgramUniforms.java new file mode 100644 index 000000000..92acb183d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/program/ProgramUniforms.java @@ -0,0 +1,361 @@ +package net.coderbot.iris.gl.program; + +import com.google.common.collect.ImmutableList; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import net.coderbot.iris.gl.uniform.DynamicLocationalUniformHolder; +import net.coderbot.iris.gl.uniform.Uniform; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformType; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.uniforms.SystemTimeUniforms; +import net.minecraft.client.Minecraft; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.ARBShaderImageLoadStore; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; + +public class ProgramUniforms { + private static ProgramUniforms active; + private final ImmutableList perTick; + private final ImmutableList perFrame; + private final ImmutableList dynamic; + private final ImmutableList notifiersToReset; + + private ImmutableList once; + long lastTick = -1; + int lastFrame = -1; + + public ProgramUniforms(ImmutableList once, ImmutableList perTick, ImmutableList perFrame, + ImmutableList dynamic, ImmutableList notifiersToReset) { + this.once = once; + this.perTick = perTick; + this.perFrame = perFrame; + this.dynamic = dynamic; + this.notifiersToReset = notifiersToReset; + } + + private void updateStage(ImmutableList uniforms) { + for (Uniform uniform : uniforms) { + uniform.update(); + } + } + + private static long getCurrentTick() { + return Objects.requireNonNull(Minecraft.getMinecraft().theWorld).getWorldTime(); + } + + public void update() { + if (active != null) { + active.removeListeners(); + } + + active = this; + + updateStage(dynamic); + + if (once != null) { + updateStage(once); + updateStage(perTick); + updateStage(perFrame); + lastTick = getCurrentTick(); + + once = null; + return; + } + + long currentTick = getCurrentTick(); + + if (lastTick != currentTick) { + lastTick = currentTick; + + updateStage(perTick); + } + + // TODO: Move the frame counter to a different place? + int currentFrame = SystemTimeUniforms.COUNTER.getAsInt(); + + if (lastFrame != currentFrame) { + lastFrame = currentFrame; + + updateStage(perFrame); + } + } + + public void removeListeners() { + active = null; + + for (ValueUpdateNotifier notifier : notifiersToReset) { + notifier.setListener(null); + } + } + + public static void clearActiveUniforms() { + if (active != null) { + active.removeListeners(); + } + } + + public static Builder builder(String name, int program) { + return new Builder(name, program); + } + + public static class Builder implements DynamicLocationalUniformHolder { + private final String name; + private final int program; + + private final Map locations; + private final Map once; + private final Map perTick; + private final Map perFrame; + private final Map dynamic; + private final Map uniformNames; + private final Map externalUniformNames; + private final List notifiersToReset; + + protected Builder(String name, int program) { + this.name = name; + this.program = program; + + locations = new HashMap<>(); + once = new HashMap<>(); + perTick = new HashMap<>(); + perFrame = new HashMap<>(); + dynamic = new HashMap<>(); + uniformNames = new HashMap<>(); + externalUniformNames = new HashMap<>(); + notifiersToReset = new ArrayList<>(); + } + + @Override + public Builder addUniform(UniformUpdateFrequency updateFrequency, Uniform uniform) { + Objects.requireNonNull(uniform); + if(uniform == null) { + throw new NullPointerException("uniform"); + } + + switch (updateFrequency) { + case ONCE: + once.put(locations.get(uniform.getLocation()), uniform); + break; + case PER_TICK: + perTick.put(locations.get(uniform.getLocation()), uniform); + break; + case PER_FRAME: + perFrame.put(locations.get(uniform.getLocation()), uniform); + break; + } + + return this; + } + + @Override + public OptionalInt location(String name, UniformType type) { + int id = IrisRenderSystem.getUniformLocation(program, name); + + if (id == -1) { + return OptionalInt.empty(); + } + + // TODO: Temporary hack until custom uniforms are merged. + if ((!locations.containsKey(id) && !uniformNames.containsKey(name)) || name.equals("framemod8")) { + locations.put(id, name); + uniformNames.put(name, type); + } else { + Iris.logger.warn("[" + this.name + "] Duplicate uniform: " + type.toString().toLowerCase() + " " + name); + + return OptionalInt.empty(); + } + + return OptionalInt.of(id); + } + + public ProgramUniforms buildUniforms() { + // Check for any unsupported uniforms and warn about them so that we can easily figure out what uniforms we need to add. + final int activeUniforms = GL20.glGetProgrami(program, GL20.GL_ACTIVE_UNIFORMS); + IntBuffer sizeType = BufferUtils.createIntBuffer(2); + + for (int index = 0; index < activeUniforms; index++) { + final String name = IrisRenderSystem.getActiveUniform(program, index, 128, sizeType); + + if (name.isEmpty()) { + // No further information available. + continue; + } + + final int size = sizeType.get(0); + final int type = sizeType.get(1); + + UniformType provided = uniformNames.get(name); + final UniformType expected = getExpectedType(type); + + if (provided == null && !name.startsWith("gl_")) { + final String typeName = getTypeName(type); + + if (isSampler(type) || isImage(type)) { + // don't print a warning, samplers and images are managed elsewhere. + // TODO: Detect unsupported samplers/images? + continue; + } + + final UniformType externalProvided = externalUniformNames.get(name); + + if (externalProvided != null) { + if (externalProvided != expected) { + String expectedName; + + if (expected != null) { + expectedName = expected.toString(); + } else { + expectedName = "(unsupported type: " + getTypeName(type) + ")"; + } + + Iris.logger.error("[" + this.name + "] Wrong uniform type for externally-managed uniform " + name + ": " + externalProvided + " is provided but the program expects " + expectedName + "."); + } + + continue; + } + + if (size == 1) { + Iris.logger.warn("[" + this.name + "] Unsupported uniform: " + typeName + " " + name); + } else { + Iris.logger.warn("[" + this.name + "] Unsupported uniform: " + name + " of size " + size + " and type " + typeName); + } + + continue; + } + + // TODO: This is an absolutely horrific hack, but is needed until custom uniforms work. + if ("framemod8".equals(name) && expected == UniformType.FLOAT && provided == UniformType.INT) { + SystemTimeUniforms.addFloatFrameMod8Uniform(this); + provided = UniformType.FLOAT; + } + + if (provided != null && provided != expected) { + String expectedName; + + if (expected != null) { + expectedName = expected.toString(); + } else { + expectedName = "(unsupported type: " + getTypeName(type) + ")"; + } + + Iris.logger.error("[" + this.name + "] Wrong uniform type for " + name + ": Iris is providing " + provided + " but the program expects " + expectedName + ". Disabling that uniform."); + + once.remove(name); + perTick.remove(name); + perFrame.remove(name); + dynamic.remove(name); + } + } + + return new ProgramUniforms(ImmutableList.copyOf(once.values()), ImmutableList.copyOf(perTick.values()), ImmutableList.copyOf(perFrame.values()), + ImmutableList.copyOf(dynamic.values()), ImmutableList.copyOf(notifiersToReset)); + } + + @Override + public Builder addDynamicUniform(Uniform uniform, ValueUpdateNotifier notifier) { + Objects.requireNonNull(uniform); + if(notifier == null){ + Iris.logger.info("notifier is null: " + uniform.getLocation()); + } + + Objects.requireNonNull(notifier); + + dynamic.put(locations.get(uniform.getLocation()), uniform); + notifiersToReset.add(notifier); + + return this; + } + + @Override + public UniformHolder externallyManagedUniform(String name, UniformType type) { + externalUniformNames.put(name, type); + + return this; + } + } + + private static String getTypeName(int type) { + return switch(type) { + case GL11.GL_FLOAT -> "float"; + case GL11.GL_INT -> "int"; + case GL20.GL_FLOAT_MAT4 -> "mat4"; + case GL20.GL_FLOAT_VEC4 -> "vec4"; + case GL20.GL_FLOAT_MAT3 -> "mat3"; + case GL20.GL_FLOAT_VEC3 -> "vec3"; + case GL20.GL_FLOAT_MAT2 -> "mat2"; + case GL20.GL_FLOAT_VEC2 -> "vec2"; + case GL20.GL_INT_VEC2 -> "ivec2"; + case GL20.GL_INT_VEC4 -> "ivec4"; + case GL20.GL_SAMPLER_3D -> "sampler3D"; + case GL20.GL_SAMPLER_2D -> "sampler2D"; + case GL30.GL_UNSIGNED_INT_SAMPLER_2D -> "usampler2D"; + case GL30.GL_UNSIGNED_INT_SAMPLER_3D -> "usampler3D"; + case GL20.GL_SAMPLER_1D -> "sampler1D"; + case GL20.GL_SAMPLER_2D_SHADOW -> "sampler2DShadow"; + case GL20.GL_SAMPLER_1D_SHADOW -> "sampler1DShadow"; + case ARBShaderImageLoadStore.GL_IMAGE_2D -> "image2D"; + case ARBShaderImageLoadStore.GL_IMAGE_3D -> "image3D"; + default -> "(unknown:" + type + ")"; + }; + } + + private static UniformType getExpectedType(int type) { + return switch (type) { + case GL11.GL_FLOAT -> UniformType.FLOAT; + case GL11.GL_INT -> UniformType.INT; + case GL20.GL_FLOAT_MAT4 -> UniformType.MAT4; + case GL20.GL_FLOAT_VEC4 -> UniformType.VEC4; + case GL20.GL_INT_VEC4 -> UniformType.VEC4I; + case GL20.GL_FLOAT_VEC3 -> UniformType.VEC3; + case GL20.GL_FLOAT_MAT3 -> null; + case GL20.GL_INT_VEC3 -> null; + case GL20.GL_FLOAT_MAT2 -> null; + case GL20.GL_FLOAT_VEC2 -> UniformType.VEC2; + case GL20.GL_INT_VEC2 -> UniformType.VEC2I; + case GL20.GL_SAMPLER_3D -> UniformType.INT; + case GL20.GL_SAMPLER_2D -> UniformType.INT; + case GL30.GL_UNSIGNED_INT_SAMPLER_2D -> UniformType.INT; + case GL30.GL_UNSIGNED_INT_SAMPLER_3D -> UniformType.INT; + case GL20.GL_SAMPLER_1D -> UniformType.INT; + case GL20.GL_SAMPLER_2D_SHADOW -> UniformType.INT; + case GL20.GL_SAMPLER_1D_SHADOW -> UniformType.INT; + default -> null; + }; + } + + private static boolean isSampler(int type) { + return type == GL20.GL_SAMPLER_1D + || type == GL20.GL_SAMPLER_2D + || type == GL30.GL_UNSIGNED_INT_SAMPLER_2D + || type == GL30.GL_UNSIGNED_INT_SAMPLER_3D + || type == GL20.GL_SAMPLER_3D + || type == GL20.GL_SAMPLER_1D_SHADOW + || type == GL20.GL_SAMPLER_2D_SHADOW; + } + + private static boolean isImage(int type) { + return type == ARBShaderImageLoadStore.GL_IMAGE_1D + || type == ARBShaderImageLoadStore.GL_IMAGE_2D + || type == ARBShaderImageLoadStore.GL_UNSIGNED_INT_IMAGE_1D + || type == ARBShaderImageLoadStore.GL_UNSIGNED_INT_IMAGE_2D + || type == ARBShaderImageLoadStore.GL_UNSIGNED_INT_IMAGE_3D + || type == ARBShaderImageLoadStore.GL_INT_IMAGE_1D + || type == ARBShaderImageLoadStore.GL_INT_IMAGE_2D + || type == ARBShaderImageLoadStore.GL_INT_IMAGE_3D + || type == ARBShaderImageLoadStore.GL_IMAGE_3D + || type == ARBShaderImageLoadStore.GL_IMAGE_1D_ARRAY + || type == ARBShaderImageLoadStore.GL_IMAGE_2D_ARRAY; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/sampler/SamplerBinding.java b/src/main/java/net/coderbot/iris/gl/sampler/SamplerBinding.java new file mode 100644 index 000000000..22aef74b2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/sampler/SamplerBinding.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.gl.sampler; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +import java.util.function.IntSupplier; + +public class SamplerBinding { + private final int textureUnit; + private final IntSupplier texture; + private final ValueUpdateNotifier notifier; + + public SamplerBinding(int textureUnit, IntSupplier texture, ValueUpdateNotifier notifier) { + this.textureUnit = textureUnit; + this.texture = texture; + this.notifier = notifier; + } + + public void update() { + updateSampler(); + + if (notifier != null) { + notifier.setListener(this::updateSampler); + } + } + + private void updateSampler() { + IrisRenderSystem.bindTextureToUnit(textureUnit, texture.getAsInt()); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/sampler/SamplerHolder.java b/src/main/java/net/coderbot/iris/gl/sampler/SamplerHolder.java new file mode 100644 index 000000000..c44aeb3d1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/sampler/SamplerHolder.java @@ -0,0 +1,21 @@ +package net.coderbot.iris.gl.sampler; + +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +import java.util.function.IntSupplier; + +public interface SamplerHolder { + void addExternalSampler(int textureUnit, String... names); + boolean hasSampler(String name); + + /** + * Like addDynamicSampler, but also ensures that any unrecognized / unbound samplers sample from this + * sampler. + * + * Throws an exception if texture unit 0 is already allocated or reserved in some way. Do not call this + * function after calls to addDynamicSampler, it must be called before any calls to addDynamicSampler. + */ + boolean addDefaultSampler(IntSupplier sampler, String... names); + boolean addDynamicSampler(IntSupplier sampler, String... names); + boolean addDynamicSampler(IntSupplier sampler, ValueUpdateNotifier notifier, String... names); +} diff --git a/src/main/java/net/coderbot/iris/gl/sampler/SamplerLimits.java b/src/main/java/net/coderbot/iris/gl/sampler/SamplerLimits.java new file mode 100644 index 000000000..4338f93f1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/sampler/SamplerLimits.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.gl.sampler; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +public class SamplerLimits { + private final int maxTextureUnits; + private final int maxDrawBuffers; + private static SamplerLimits instance; + + private SamplerLimits() { + this.maxTextureUnits = GL11.glGetInteger(GL20.GL_MAX_TEXTURE_IMAGE_UNITS); + this.maxDrawBuffers = GL11.glGetInteger(GL20.GL_MAX_DRAW_BUFFERS); + } + + public int getMaxTextureUnits() { + return maxTextureUnits; + } + + public int getMaxDrawBuffers() { + return maxDrawBuffers; + } + + public static SamplerLimits get() { + if (instance == null) { + instance = new SamplerLimits(); + } + + return instance; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/shader/GlShader.java b/src/main/java/net/coderbot/iris/gl/shader/GlShader.java new file mode 100644 index 000000000..7ae367108 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/shader/GlShader.java @@ -0,0 +1,67 @@ +// This file is based on code from Sodium by JellySquid, licensed under the LGPLv3 license. + +package net.coderbot.iris.gl.shader; + +import net.coderbot.iris.gl.GLDebug; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.KHRDebug; + +import java.util.Locale; + +/** + * A compiled OpenGL shader object. + */ +public class GlShader extends GlResource { + private static final Logger LOGGER = LogManager.getLogger(GlShader.class); + + private final String name; + + public GlShader(ShaderType type, String name, String src) { + super(createShader(type, name, src)); + + this.name = name; + } + + private static int createShader(ShaderType type, String name, String src) { + int handle = GL20.glCreateShader(type.id); + // TODO: Iris +// ShaderWorkarounds.safeShaderSource(handle, src); + // TODO: ShaderWorkaround + GL20.glShaderSource(handle, src + '\0'); + GL20.glCompileShader(handle); + + GLDebug.nameObject(KHRDebug.GL_SHADER, handle, name + "(" + type.name().toLowerCase(Locale.ROOT) + ")"); + + String log = IrisRenderSystem.getShaderInfoLog(handle); + + if (!log.isEmpty()) { + LOGGER.warn("Shader compilation log for " + name + ": " + log); + } + + int result = GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS); + + if (result != GL11.GL_TRUE) { + throw new RuntimeException("Shader compilation failed, see log for details"); + } + + return handle; + } + + public String getName() { + return this.name; + } + + public int getHandle() { + return this.getGlId(); + } + + @Override + protected void destroyInternal() { + GL20.glDeleteShader(this.getGlId()); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/shader/ProgramCreator.java b/src/main/java/net/coderbot/iris/gl/shader/ProgramCreator.java new file mode 100644 index 000000000..26c47dbe6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/shader/ProgramCreator.java @@ -0,0 +1,52 @@ +// This file is based on code from Sodium by JellySquid, licensed under the LGPLv3 license. + +package net.coderbot.iris.gl.shader; + +import net.coderbot.iris.gl.GLDebug; +import net.coderbot.iris.gl.IrisRenderSystem; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.KHRDebug; + +public class ProgramCreator { + private static final Logger LOGGER = LogManager.getLogger(ProgramCreator.class); + + public static int create(String name, GlShader... shaders) { + int program = GL20.glCreateProgram(); + + // TODO: This is *really* hardcoded, we need to refactor this to support external calls to glBindAttribLocation + IrisRenderSystem.bindAttributeLocation(program, 11, "mc_Entity"); + IrisRenderSystem.bindAttributeLocation(program, 12, "mc_midTexCoord"); + IrisRenderSystem.bindAttributeLocation(program, 13, "at_tangent"); + IrisRenderSystem.bindAttributeLocation(program, 14, "at_midBlock"); + + for (GlShader shader : shaders) { + GL20.glAttachShader(program, shader.getHandle()); + } + + GL20.glLinkProgram(program); + + GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, name); + + //Always detach shaders according to https://www.khronos.org/opengl/wiki/Shader_Compilation#Cleanup + for (GlShader shader : shaders) { + IrisRenderSystem.detachShader(program, shader.getHandle()); + } + + String log = IrisRenderSystem.getProgramInfoLog(program); + + if (!log.isEmpty()) { + LOGGER.warn("Program link log for " + name + ": " + log); + } + + int result = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS); + + if (result != GL11.GL_TRUE) { + throw new RuntimeException("Shader program linking failed, see log for details"); + } + + return program; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/shader/ShaderType.java b/src/main/java/net/coderbot/iris/gl/shader/ShaderType.java new file mode 100644 index 000000000..49d621f11 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/shader/ShaderType.java @@ -0,0 +1,23 @@ +// This file is based on code from Sodium by JellySquid, licensed under the LGPLv3 license. + +package net.coderbot.iris.gl.shader; + +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL32; +import org.lwjgl.opengl.GL43; + +/** + * An enumeration over the supported OpenGL shader types. + */ +public enum ShaderType { + VERTEX(GL20.GL_VERTEX_SHADER), + GEOMETRY(GL32.GL_GEOMETRY_SHADER), + FRAGMENT(GL20.GL_FRAGMENT_SHADER), + COMPUTE(GL43.GL_COMPUTE_SHADER); + + public final int id; + + ShaderType(int id) { + this.id = id; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/shader/StandardMacros.java b/src/main/java/net/coderbot/iris/gl/shader/StandardMacros.java new file mode 100644 index 000000000..e84a5c776 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/shader/StandardMacros.java @@ -0,0 +1,265 @@ +package net.coderbot.iris.gl.shader; + +import com.google.common.collect.ImmutableList; +import cpw.mods.fml.common.Loader; +import net.coderbot.iris.pipeline.HandRenderer; +import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.shaderpack.StringPair; +import net.coderbot.iris.texture.format.TextureFormat; +import net.coderbot.iris.texture.format.TextureFormatLoader; +import org.lwjgl.LWJGLUtil; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class StandardMacros { + private static final Pattern SEMVER_PATTERN = Pattern.compile("(?\\d+)\\.(?\\d+)\\.*(?\\d*)(.*)"); + + private static void define(List defines, String key) { + defines.add(new StringPair(key, "")); + } + + private static void define(List defines, String key, String value) { + defines.add(new StringPair(key, value)); + } + + public static Iterable createStandardEnvironmentDefines() { + ArrayList standardDefines = new ArrayList<>(); + + define(standardDefines, "MC_VERSION", getMcVersion()); + define(standardDefines, "MC_GL_VERSION", getGlVersion(GL11.GL_VERSION)); + define(standardDefines, "MC_GLSL_VERSION", getGlVersion(GL20.GL_SHADING_LANGUAGE_VERSION)); + define(standardDefines, getOsString()); + define(standardDefines, getVendor()); + define(standardDefines, getRenderer()); + + for (String glExtension : getGlExtensions()) { + define(standardDefines, glExtension); + } + + define(standardDefines, "MC_NORMAL_MAP"); + define(standardDefines, "MC_SPECULAR_MAP"); + define(standardDefines, "MC_RENDER_QUALITY", "1.0"); + define(standardDefines, "MC_SHADOW_QUALITY", "1.0"); + define(standardDefines, "MC_HAND_DEPTH", Float.toString(HandRenderer.DEPTH)); + + TextureFormat textureFormat = TextureFormatLoader.getFormat(); + if (textureFormat != null) { + for (String define : textureFormat.getDefines()) { + define(standardDefines, define); + } + } + + getRenderStages().forEach((stage, index) -> define(standardDefines, stage, index)); + + for (String irisDefine : getIrisDefines()) { + define(standardDefines, irisDefine); + } + + return ImmutableList.copyOf(standardDefines); + } + + /** + * Gets the current mc version String in a 5 digit format + * + * @return mc version string + * @see Optifine Doc + */ + public static String getMcVersion() { + final String version = Loader.MC_VERSION;; + + String[] splitVersion = version.split("\\."); + + if (splitVersion.length < 2) { + throw new IllegalStateException("Could not parse game version \"" + version + "\""); + } + + String major = splitVersion[0]; + String minor = splitVersion[1]; + String bugfix; + + if (splitVersion.length < 3) { + bugfix = "00"; + } else { + bugfix = splitVersion[2]; + } + + if (minor.length() == 1) { + minor = 0 + minor; + } + if (bugfix.length() == 1) { + bugfix = 0 + bugfix; + } + + return major + minor + bugfix; + } + + /** + * Returns the current GL Version using regex + * + * @param name the name of the gl attribute to parse + * @return current gl version stripped of semantic versioning + * @see Optifine Doc for GL Version + * @see Optifine Doc for GLSL Version + */ + public static String getGlVersion(int name) { + final String info = GL11.glGetString(name); + + Matcher matcher = SEMVER_PATTERN.matcher(Objects.requireNonNull(info)); + + if (!matcher.matches()) { + throw new IllegalStateException("Could not parse GL version from \"" + info + "\""); + } + + String major = group(matcher, "major"); + String minor = group(matcher, "minor"); + String bugfix = group(matcher, "bugfix"); + + if (bugfix == null) { + // if bugfix is not there, it is 0 + bugfix = "0"; + } + + if (major == null || minor == null) { + throw new IllegalStateException("Could not parse GL version from \"" + info + "\""); + } + + return major + minor + bugfix; + } + + /** + * Expanded version of {@link Matcher#group(String)} that does not throw an exception. + * If the argument is incorrect (normally resulting in an exception), it returns null + * + * @param matcher matcher to check the group by + * @param name name of the group + * @return the section of the matcher that is a group, or null, if that matcher does not contain said group + */ + public static String group(Matcher matcher, String name) { + try { + return matcher.group(name); + } catch (IllegalArgumentException | IllegalStateException exception) { + return null; + } + } + + /** + * Returns the current OS String + * + * @return the string based on the current OS + * @see Optifine Doc + */ + public static String getOsString() { + return switch (LWJGLUtil.getPlatform()) { + case LWJGLUtil.PLATFORM_MACOSX -> "MC_OS_MAC"; + case LWJGLUtil.PLATFORM_LINUX -> "MC_OS_LINUX"; + case LWJGLUtil.PLATFORM_WINDOWS -> "MC_OS_WINDOWS"; + default -> "MC_OS_UNKNOWN"; + }; + } + + /** + * Returns a string indicating the graphics card being used + * + * @return the graphics card prefixed with "MC_GL_VENDOR_" + * @see Optifine Doc + */ + public static String getVendor() { + String vendor = Objects.requireNonNull(GL11.glGetString(GL11.GL_VENDOR)).toLowerCase(Locale.ROOT); + if (vendor.startsWith("ati")) { + return "MC_GL_VENDOR_ATI"; + } else if (vendor.startsWith("intel")) { + return "MC_GL_VENDOR_INTEL"; + } else if (vendor.startsWith("nvidia")) { + return "MC_GL_VENDOR_NVIDIA"; + } else if (vendor.startsWith("amd")) { + return "MC_GL_VENDOR_AMD"; + } else if (vendor.startsWith("x.org")) { + return "MC_GL_VENDOR_XORG"; + } + return "MC_GL_VENDOR_OTHER"; + } + + /** + * Returns the graphics driver being used + * + * @return graphics driver prefixed with "MC_GL_RENDERER_" + * @see Optifine Doc + */ + public static String getRenderer() { + String renderer = Objects.requireNonNull(GL11.glGetString(GL11.GL_RENDERER)).toLowerCase(Locale.ROOT); + if (renderer.startsWith("amd")) { + return "MC_GL_RENDERER_RADEON"; + } else if (renderer.startsWith("ati")) { + return "MC_GL_RENDERER_RADEON"; + } else if (renderer.startsWith("radeon")) { + return "MC_GL_RENDERER_RADEON"; + } else if (renderer.startsWith("gallium")) { + return "MC_GL_RENDERER_GALLIUM"; + } else if (renderer.startsWith("intel")) { + return "MC_GL_RENDERER_INTEL"; + } else if (renderer.startsWith("geforce")) { + return "MC_GL_RENDERER_GEFORCE"; + } else if (renderer.startsWith("nvidia")) { + return "MC_GL_RENDERER_GEFORCE"; + } else if (renderer.startsWith("quadro")) { + return "MC_GL_RENDERER_QUADRO"; + } else if (renderer.startsWith("nvs")) { + return "MC_GL_RENDERER_QUADRO"; + } else if (renderer.startsWith("mesa")) { + return "MC_GL_RENDERER_MESA"; + } + return "MC_GL_RENDERER_OTHER"; + } + + /** + * Returns the list of currently enabled GL extensions + * This is done by calling {@link GL11#glGetString} with the arg {@link GL11#GL_EXTENSIONS} + * + * @return list of activated extensions prefixed with "MC_" + * @see Optifine Doc + */ + public static Set getGlExtensions() { + String[] extensions = Objects.requireNonNull(GL11.glGetString(GL11.GL_EXTENSIONS)).split("\\s+"); + + // TODO note that we do not add extensions based on if the shader uses them and if they are supported + // see https://github.com/sp614x/optifine/blob/master/OptiFineDoc/doc/shaders.txt#L738 + + // NB: Use Collectors.toSet(). In some cases, there are duplicate extensions in the extension list. + // RenderDoc is one example - it causes the GL_KHR_debug extension to appear twice: + + return Arrays.stream(extensions).map(s -> "MC_" + s).collect(Collectors.toSet()); + } + + public static Map getRenderStages() { + Map stages = new HashMap<>(); + for (WorldRenderingPhase phase : WorldRenderingPhase.values()) { + stages.put("MC_RENDER_STAGE_" + phase.name(), String.valueOf(phase.ordinal())); + } + return stages; + } + + /** + * Returns the list of Iris-exclusive uniforms supported in the current version of Iris. + * + * @return List of definitions corresponding to the uniform names prefixed with "MC_" + */ + public static List getIrisDefines() { + List defines = new ArrayList<>(); + // All Iris-exclusive uniforms should have a corresponding definition here. Example: + // defines.add("MC_UNIFORM_DRAGON_DEATH_PROGRESS"); + + return defines; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/state/StateUpdateNotifiers.java b/src/main/java/net/coderbot/iris/gl/state/StateUpdateNotifiers.java new file mode 100644 index 000000000..7c1f535f3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/state/StateUpdateNotifiers.java @@ -0,0 +1,18 @@ +package net.coderbot.iris.gl.state; + +/** + * Holds some standard update notifiers for various elements of GL state. Currently, this class has a few listeners for + * fog-related values. + */ +public class StateUpdateNotifiers { + public static ValueUpdateNotifier fogToggleNotifier; + public static ValueUpdateNotifier fogModeNotifier; + public static ValueUpdateNotifier fogStartNotifier; + public static ValueUpdateNotifier fogEndNotifier; + public static ValueUpdateNotifier fogDensityNotifier; + public static ValueUpdateNotifier blendFuncNotifier; + public static ValueUpdateNotifier bindTextureNotifier; + public static ValueUpdateNotifier normalTextureChangeNotifier; + public static ValueUpdateNotifier specularTextureChangeNotifier; + public static ValueUpdateNotifier phaseChangeNotifier; +} diff --git a/src/main/java/net/coderbot/iris/gl/state/ValueUpdateNotifier.java b/src/main/java/net/coderbot/iris/gl/state/ValueUpdateNotifier.java new file mode 100644 index 000000000..81774db5a --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/state/ValueUpdateNotifier.java @@ -0,0 +1,12 @@ +package net.coderbot.iris.gl.state; + +/** + * A + */ +public interface ValueUpdateNotifier { + /** + * Sets up a listener with this notifier. Whenever the underlying value of + * @param listener + */ + void setListener(Runnable listener); +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/DepthBufferFormat.java b/src/main/java/net/coderbot/iris/gl/texture/DepthBufferFormat.java new file mode 100644 index 000000000..6b06c2472 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/DepthBufferFormat.java @@ -0,0 +1,80 @@ +package net.coderbot.iris.gl.texture; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL30; + +public enum DepthBufferFormat { + DEPTH(false), + DEPTH16(false), + DEPTH24(false), + DEPTH32(false), + DEPTH32F(false), + DEPTH_STENCIL(true), + DEPTH24_STENCIL8(true), + DEPTH32F_STENCIL8(true); + + private final boolean combinedStencil; + + DepthBufferFormat(boolean combinedStencil) { + this.combinedStencil = combinedStencil; + } + + @Nullable + public static DepthBufferFormat fromGlEnum(int glenum) { + return switch (glenum) { + case GL11.GL_DEPTH_COMPONENT -> DepthBufferFormat.DEPTH; + case GL14.GL_DEPTH_COMPONENT16 -> DepthBufferFormat.DEPTH16; + case GL14.GL_DEPTH_COMPONENT24 -> DepthBufferFormat.DEPTH24; + case GL14.GL_DEPTH_COMPONENT32 -> DepthBufferFormat.DEPTH32; + case GL30.GL_DEPTH_COMPONENT32F -> DepthBufferFormat.DEPTH32F; + case GL30.GL_DEPTH_STENCIL -> DepthBufferFormat.DEPTH_STENCIL; + case GL30.GL_DEPTH24_STENCIL8 -> DepthBufferFormat.DEPTH24_STENCIL8; + case GL30.GL_DEPTH32F_STENCIL8 -> DepthBufferFormat.DEPTH32F_STENCIL8; + default -> null; + }; + } + + public static DepthBufferFormat fromGlEnumOrDefault(int glenum) { + DepthBufferFormat format = fromGlEnum(glenum); + if (format == null) { + // yolo, just assume it's GL_DEPTH_COMPONENT + return DepthBufferFormat.DEPTH; + } + return format; + } + + public int getGlInternalFormat() { + return switch (this) { + case DEPTH -> GL11.GL_DEPTH_COMPONENT; + case DEPTH16 -> GL14.GL_DEPTH_COMPONENT16; + case DEPTH24 -> GL14.GL_DEPTH_COMPONENT24; + case DEPTH32 -> GL14.GL_DEPTH_COMPONENT32; + case DEPTH32F -> GL30.GL_DEPTH_COMPONENT32F; + case DEPTH_STENCIL -> GL30.GL_DEPTH_STENCIL; + case DEPTH24_STENCIL8 -> GL30.GL_DEPTH24_STENCIL8; + case DEPTH32F_STENCIL8 -> GL30.GL_DEPTH32F_STENCIL8; + }; + + } + + public int getGlType() { + return isCombinedStencil() ? GL30.GL_DEPTH_STENCIL : GL11.GL_DEPTH_COMPONENT; + } + + public int getGlFormat() { + return switch (this) { + case DEPTH, DEPTH16 -> GL11.GL_UNSIGNED_SHORT; + case DEPTH24, DEPTH32 -> GL11.GL_UNSIGNED_INT; + case DEPTH32F -> GL11.GL_FLOAT; + case DEPTH_STENCIL, DEPTH24_STENCIL8 -> GL30.GL_UNSIGNED_INT_24_8; + case DEPTH32F_STENCIL8 -> GL30.GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + }; + + } + + public boolean isCombinedStencil() { + return combinedStencil; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/DepthCopyStrategy.java b/src/main/java/net/coderbot/iris/gl/texture/DepthCopyStrategy.java new file mode 100644 index 000000000..e189fec1c --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/DepthCopyStrategy.java @@ -0,0 +1,103 @@ +package net.coderbot.iris.gl.texture; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL43; + +public interface DepthCopyStrategy { + // FB -> T + class Gl20CopyTexture implements DepthCopyStrategy { + private Gl20CopyTexture() { + // private + } + + @Override + public boolean needsDestFramebuffer() { + return false; + } + + @Override + public void copy(GlFramebuffer sourceFb, int sourceTexture, GlFramebuffer destFb, int destTexture, int width, int height) { + sourceFb.bindAsReadBuffer(); + + IrisRenderSystem.copyTexSubImage2D( + destTexture, + // target + GL11.GL_TEXTURE_2D, + // level + 0, + // xoffset, yoffset + 0, 0, + // x, y + 0, 0, + // width + width, + // height + height); + } + } + + // FB -> FB + class Gl30BlitFbCombinedDepthStencil implements DepthCopyStrategy { + private Gl30BlitFbCombinedDepthStencil() { + // private + } + + @Override + public boolean needsDestFramebuffer() { + return true; + } + + @Override + public void copy(GlFramebuffer sourceFb, int sourceTexture, GlFramebuffer destFb, int destTexture, int width, int height) { + IrisRenderSystem.blitFramebuffer(sourceFb.getId(), destFb.getId(), 0, 0, width, height, + 0, 0, width, height, + GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT, + GL11.GL_NEAREST); + } + } + + // T -> T + // Fastest + class Gl43CopyImage implements DepthCopyStrategy { + private Gl43CopyImage() { + // private + } + + @Override + public boolean needsDestFramebuffer() { + return false; + } + + @Override + public void copy(GlFramebuffer sourceFb, int sourceTexture, GlFramebuffer destFb, int destTexture, int width, int height) { + GL43.glCopyImageSubData(sourceTexture, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, destTexture, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, width, height, 1); + } + } + + static DepthCopyStrategy fastest(boolean combinedStencilRequired) { + if (Iris.capabilities.GL_ARB_copy_image) { + return new Gl43CopyImage(); + } + + if (combinedStencilRequired) { + return new Gl30BlitFbCombinedDepthStencil(); + } else { + return new Gl20CopyTexture(); + } + } + + boolean needsDestFramebuffer(); + + /** + * Executes the copy. May or may not clobber GL_READ_FRAMEBUFFER and GL_DRAW_FRAMEBUFFER bindings - the caller is + * responsible for ensuring that they are restored to sensible values, or that the previous values are not relied + * on. The callee is responsible for ensuring that texture bindings are not modified. + * + * @param destFb The destination framebuffer. If {@link #needsDestFramebuffer()} returns false, then this param + * will not be used, and it can be null. + */ + void copy(GlFramebuffer sourceFb, int sourceTexture, GlFramebuffer destFb, int destTexture, int width, int height); +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/InternalTextureFormat.java b/src/main/java/net/coderbot/iris/gl/texture/InternalTextureFormat.java new file mode 100644 index 000000000..5989fd543 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/InternalTextureFormat.java @@ -0,0 +1,108 @@ +package net.coderbot.iris.gl.texture; + +import net.coderbot.iris.gl.GlVersion; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL31; + +import java.util.Optional; + +public enum InternalTextureFormat { + // Default + // TODO: This technically shouldn't be exposed to shaders since it's not in the specification, it's the default anyways + RGBA(GL11.GL_RGBA, GlVersion.GL_11, PixelFormat.RGBA), + // 8-bit normalized + R8(GL30.GL_R8, GlVersion.GL_30, PixelFormat.RED), + RG8(GL30.GL_RG8, GlVersion.GL_30, PixelFormat.RG), + RGB8(GL11.GL_RGB8, GlVersion.GL_11, PixelFormat.RGB), + RGBA8(GL11.GL_RGBA8, GlVersion.GL_11, PixelFormat.RGBA), + // 8-bit signed normalized + R8_SNORM(GL31.GL_R8_SNORM, GlVersion.GL_31, PixelFormat.RED), + RG8_SNORM(GL31.GL_RG8_SNORM, GlVersion.GL_31, PixelFormat.RG), + RGB8_SNORM(GL31.GL_RGB8_SNORM, GlVersion.GL_31, PixelFormat.RGB), + RGBA8_SNORM(GL31.GL_RGBA8_SNORM, GlVersion.GL_31, PixelFormat.RGBA), + // 16-bit normalized + R16(GL30.GL_R16, GlVersion.GL_30, PixelFormat.RED), + RG16(GL30.GL_RG16, GlVersion.GL_30, PixelFormat.RG), + RGB16(GL11.GL_RGB16, GlVersion.GL_11, PixelFormat.RGB), + RGBA16(GL11.GL_RGBA16, GlVersion.GL_11, PixelFormat.RGBA), + // 16-bit signed normalized + R16_SNORM(GL31.GL_R16_SNORM, GlVersion.GL_31, PixelFormat.RED), + RG16_SNORM(GL31.GL_RG16_SNORM, GlVersion.GL_31, PixelFormat.RG), + RGB16_SNORM(GL31.GL_RGB16_SNORM, GlVersion.GL_31, PixelFormat.RGB), + RGBA16_SNORM(GL31.GL_RGBA16_SNORM, GlVersion.GL_31, PixelFormat.RGBA), + // 16-bit float + R16F(GL30.GL_R16F, GlVersion.GL_30, PixelFormat.RED), + RG16F(GL30.GL_RG16F, GlVersion.GL_30, PixelFormat.RG), + RGB16F(GL30.GL_RGB16F, GlVersion.GL_30, PixelFormat.RGB), + RGBA16F(GL30.GL_RGBA16F, GlVersion.GL_30, PixelFormat.RGBA), + // 32-bit float + R32F(GL30.GL_R32F, GlVersion.GL_30, PixelFormat.RED), + RG32F(GL30.GL_RG32F, GlVersion.GL_30, PixelFormat.RG), + RGB32F(GL30.GL_RGB32F, GlVersion.GL_30, PixelFormat.RGB), + RGBA32F(GL30.GL_RGBA32F, GlVersion.GL_30, PixelFormat.RGBA), + // 8-bit integer + R8I(GL30.GL_R8I, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG8I(GL30.GL_RG8I, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB8I(GL30.GL_RGB8I, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA8I(GL30.GL_RGBA8I, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // 8-bit unsigned integer + R8UI(GL30.GL_R8UI, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG8UI(GL30.GL_RG8UI, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB8UI(GL30.GL_RGB8UI, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA8UI(GL30.GL_RGBA8UI, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // 16-bit integer + R16I(GL30.GL_R16I, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG16I(GL30.GL_RG16I, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB16I(GL30.GL_RGB16I, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA16I(GL30.GL_RGBA16I, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // 16-bit unsigned integer + R16UI(GL30.GL_R16UI, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG16UI(GL30.GL_RG16UI, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB16UI(GL30.GL_RGB16UI, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA16UI(GL30.GL_RGBA16UI, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // 32-bit integer + R32I(GL30.GL_R32I, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG32I(GL30.GL_RG32I, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB32I(GL30.GL_RGB32I, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA32I(GL30.GL_RGBA32I, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // 32-bit unsigned integer + R32UI(GL30.GL_R32UI, GlVersion.GL_30, PixelFormat.RED_INTEGER), + RG32UI(GL30.GL_RG32UI, GlVersion.GL_30, PixelFormat.RG_INTEGER), + RGB32UI(GL30.GL_RGB32UI, GlVersion.GL_30, PixelFormat.RGB_INTEGER), + RGBA32UI(GL30.GL_RGBA32UI, GlVersion.GL_30, PixelFormat.RGBA_INTEGER), + // Mixed + R3_G3_B2(GL11.GL_R3_G3_B2, GlVersion.GL_11, PixelFormat.RGB), + RGB5_A1(GL11.GL_RGB5_A1, GlVersion.GL_11, PixelFormat.RGBA), + RGB10_A2(GL11.GL_RGB10_A2, GlVersion.GL_11, PixelFormat.RGBA), + R11F_G11F_B10F(GL30.GL_R11F_G11F_B10F, GlVersion.GL_30, PixelFormat.RGB), + RGB9_E5(GL30.GL_RGB9_E5, GlVersion.GL_30, PixelFormat.RGB); + + private final int glFormat; + private final GlVersion minimumGlVersion; + private final PixelFormat expectedPixelFormat; + + InternalTextureFormat(int glFormat, GlVersion minimumGlVersion, PixelFormat expectedPixelFormat) { + this.glFormat = glFormat; + this.minimumGlVersion = minimumGlVersion; + this.expectedPixelFormat = expectedPixelFormat; + } + + public static Optional fromString(String name) { + try { + return Optional.of(InternalTextureFormat.valueOf(name)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + public int getGlFormat() { + return glFormat; + } + + public PixelFormat getPixelFormat() { return expectedPixelFormat; } + + public GlVersion getMinimumGlVersion() { + return minimumGlVersion; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/PixelFormat.java b/src/main/java/net/coderbot/iris/gl/texture/PixelFormat.java new file mode 100644 index 000000000..338dd3f7c --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/PixelFormat.java @@ -0,0 +1,53 @@ +package net.coderbot.iris.gl.texture; + +import net.coderbot.iris.gl.GlVersion; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL30; + +import java.util.Optional; + +public enum PixelFormat { + RED(GL11.GL_RED, GlVersion.GL_11, false), + RG(GL30.GL_RG, GlVersion.GL_30, false), + RGB(GL11.GL_RGB, GlVersion.GL_11, false), + BGR(GL12.GL_BGR, GlVersion.GL_12, false), + RGBA(GL11.GL_RGBA, GlVersion.GL_11, false), + BGRA(GL12.GL_BGRA, GlVersion.GL_12, false), + RED_INTEGER(GL30.GL_RED_INTEGER, GlVersion.GL_30, true), + RG_INTEGER(GL30.GL_RG_INTEGER, GlVersion.GL_30, true), + RGB_INTEGER(GL30.GL_RGB_INTEGER, GlVersion.GL_30, true), + BGR_INTEGER(GL30.GL_BGR_INTEGER, GlVersion.GL_30, true), + RGBA_INTEGER(GL30.GL_RGBA_INTEGER, GlVersion.GL_30, true), + BGRA_INTEGER(GL30.GL_BGRA_INTEGER, GlVersion.GL_30, true); + + private final int glFormat; + private final GlVersion minimumGlVersion; + private final boolean isInteger; + + PixelFormat(int glFormat, GlVersion minimumGlVersion, boolean isInteger) { + this.glFormat = glFormat; + this.minimumGlVersion = minimumGlVersion; + this.isInteger = isInteger; + } + + public static Optional fromString(String name) { + try { + return Optional.of(PixelFormat.valueOf(name)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + public int getGlFormat() { + return glFormat; + } + + public GlVersion getMinimumGlVersion() { + return minimumGlVersion; + } + + public boolean isInteger() { + return isInteger; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/PixelType.java b/src/main/java/net/coderbot/iris/gl/texture/PixelType.java new file mode 100644 index 000000000..7457b2faf --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/PixelType.java @@ -0,0 +1,55 @@ +package net.coderbot.iris.gl.texture; + +import net.coderbot.iris.gl.GlVersion; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL30; + +import java.util.Optional; + +public enum PixelType { + BYTE(GL11.GL_BYTE, GlVersion.GL_11), + SHORT(GL11.GL_SHORT, GlVersion.GL_11), + INT(GL11.GL_INT, GlVersion.GL_11), + HALF_FLOAT(GL30.GL_HALF_FLOAT, GlVersion.GL_30), + FLOAT(GL11.GL_FLOAT, GlVersion.GL_11), + UNSIGNED_BYTE(GL11.GL_UNSIGNED_BYTE, GlVersion.GL_11), + UNSIGNED_BYTE_3_3_2(GL12.GL_UNSIGNED_BYTE_3_3_2, GlVersion.GL_12), + UNSIGNED_BYTE_2_3_3_REV(GL12.GL_UNSIGNED_BYTE_2_3_3_REV, GlVersion.GL_12), + UNSIGNED_SHORT(GL11.GL_UNSIGNED_SHORT, GlVersion.GL_11), + UNSIGNED_SHORT_5_6_5(GL12.GL_UNSIGNED_SHORT_5_6_5, GlVersion.GL_12), + UNSIGNED_SHORT_5_6_5_REV(GL12.GL_UNSIGNED_SHORT_5_6_5_REV, GlVersion.GL_12), + UNSIGNED_SHORT_4_4_4_4(GL12.GL_UNSIGNED_SHORT_4_4_4_4, GlVersion.GL_12), + UNSIGNED_SHORT_4_4_4_4_REV(GL12.GL_UNSIGNED_SHORT_4_4_4_4_REV, GlVersion.GL_12), + UNSIGNED_SHORT_5_5_5_1(GL12.GL_UNSIGNED_SHORT_5_5_5_1, GlVersion.GL_12), + UNSIGNED_SHORT_1_5_5_5_REV(GL12.GL_UNSIGNED_SHORT_1_5_5_5_REV, GlVersion.GL_12), + UNSIGNED_INT(GL11.GL_UNSIGNED_BYTE, GlVersion.GL_11), + UNSIGNED_INT_8_8_8_8(GL12.GL_UNSIGNED_INT_8_8_8_8, GlVersion.GL_12), + UNSIGNED_INT_8_8_8_8_REV(GL12.GL_UNSIGNED_INT_8_8_8_8_REV, GlVersion.GL_12), + UNSIGNED_INT_10_10_10_2(GL12.GL_UNSIGNED_INT_10_10_10_2, GlVersion.GL_12), + UNSIGNED_INT_2_10_10_10_REV(GL12.GL_UNSIGNED_INT_2_10_10_10_REV, GlVersion.GL_12); + + private final int glFormat; + private final GlVersion minimumGlVersion; + + PixelType(int glFormat, GlVersion minimumGlVersion) { + this.glFormat = glFormat; + this.minimumGlVersion = minimumGlVersion; + } + + public static Optional fromString(String name) { + try { + return Optional.of(PixelType.valueOf(name)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + public int getGlFormat() { + return glFormat; + } + + public GlVersion getMinimumGlVersion() { + return minimumGlVersion; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/TextureScaleOverride.java b/src/main/java/net/coderbot/iris/gl/texture/TextureScaleOverride.java new file mode 100644 index 000000000..e978ba1b5 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/TextureScaleOverride.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.gl.texture; + +public class TextureScaleOverride { + public float relativeX, relativeY; + public int sizeX, sizeY; + public final boolean isXRelative, isYRelative; + + public TextureScaleOverride(String xValue, String yValue) { + if (xValue.contains(".")) { + this.relativeX = Float.parseFloat(xValue); + this.isXRelative = true; + } else { + this.sizeX = Integer.parseInt(xValue); + this.isXRelative = false; + } + + if (yValue.contains(".")) { + this.relativeY = Float.parseFloat(yValue); + this.isYRelative = true; + } else { + this.sizeY = Integer.parseInt(yValue); + this.isYRelative = false; + } + } + + public int getX(int originalX) { + if (isXRelative) { + return (int) (originalX * relativeX); + } else { + return sizeX; + } + } + + public int getY(int originalY) { + if (isYRelative) { + return (int) (originalY * relativeY); + } else { + return sizeY; + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/texture/TextureUploadHelper.java b/src/main/java/net/coderbot/iris/gl/texture/TextureUploadHelper.java new file mode 100644 index 000000000..cfc97f19d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/texture/TextureUploadHelper.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.gl.texture; + +import org.lwjgl.opengl.GL11; + +public class TextureUploadHelper { + private TextureUploadHelper() { + // no construction + } + + public static void resetTextureUploadState() { + // Ensure that the pixel storage mode is in a sane state, otherwise the uploaded texture data will be quite + // incorrect. + // + // It is likely that this also avoids the crashes on AMD that I previously experienced with texture creation. + // + // This code is from Canvas: https://github.com/grondag/canvas/commit/f0ab652d7a8b7cc9febf0209bee15cffce9eac83 + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0); + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/BooleanUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/BooleanUniform.java new file mode 100644 index 000000000..340575a69 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/BooleanUniform.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.gl.uniform; + +import java.util.function.BooleanSupplier; + +public class BooleanUniform extends IntUniform { + BooleanUniform(int location, BooleanSupplier value) { + super(location, () -> value.getAsBoolean() ? 1 : 0); + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/DynamicLocationalUniformHolder.java b/src/main/java/net/coderbot/iris/gl/uniform/DynamicLocationalUniformHolder.java new file mode 100644 index 000000000..12c678b0d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/DynamicLocationalUniformHolder.java @@ -0,0 +1,56 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import org.joml.Vector2i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public interface DynamicLocationalUniformHolder extends LocationalUniformHolder, DynamicUniformHolder { + DynamicLocationalUniformHolder addDynamicUniform(Uniform uniform, ValueUpdateNotifier notifier); + + default DynamicLocationalUniformHolder uniform1f(String name, FloatSupplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.FLOAT).ifPresent(id -> addDynamicUniform(new FloatUniform(id, value, notifier), notifier)); + + return this; + } + + default DynamicLocationalUniformHolder uniform1f(String name, IntSupplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.FLOAT).ifPresent(id -> addDynamicUniform(new FloatUniform(id, () -> (float) value.getAsInt(), notifier), notifier)); + + return this; + } + + default DynamicLocationalUniformHolder uniform1f(String name, DoubleSupplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.FLOAT).ifPresent(id -> addDynamicUniform(new FloatUniform(id, () -> (float) value.getAsDouble(), notifier), notifier)); + + return this; + } + + default DynamicLocationalUniformHolder uniform1i(String name, IntSupplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.INT).ifPresent(id -> addDynamicUniform(new IntUniform(id, value, notifier), notifier)); + + return this; + } + + default DynamicLocationalUniformHolder uniform2i(String name, Supplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.VEC2I).ifPresent(id -> addDynamicUniform(new Vector2IntegerJomlUniform(id, value, notifier), notifier)); + + return this; + } + + default DynamicUniformHolder uniform4f(String name, Supplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.VEC4).ifPresent(id -> addDynamicUniform(new Vector4Uniform(id, value, notifier), notifier)); + + return this; + } + + default DynamicUniformHolder uniform4i(String name, Supplier value, ValueUpdateNotifier notifier) { + location(name, UniformType.VEC4I).ifPresent(id -> addDynamicUniform(new Vector4IntegerJomlUniform(id, value, notifier), notifier)); + + return this; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/DynamicUniformHolder.java b/src/main/java/net/coderbot/iris/gl/uniform/DynamicUniformHolder.java new file mode 100644 index 000000000..c921ef6b2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/DynamicUniformHolder.java @@ -0,0 +1,20 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import org.joml.Vector2i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public interface DynamicUniformHolder extends UniformHolder { + DynamicUniformHolder uniform1f(String name, FloatSupplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform1f(String name, IntSupplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform1f(String name, DoubleSupplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform1i(String name, IntSupplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform2i(String name, Supplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform4f(String name, Supplier value, ValueUpdateNotifier notifier); + DynamicUniformHolder uniform4i(String name, Supplier value, ValueUpdateNotifier notifier); +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/FloatSupplier.java b/src/main/java/net/coderbot/iris/gl/uniform/FloatSupplier.java new file mode 100644 index 000000000..9a8ccd4f8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/FloatSupplier.java @@ -0,0 +1,6 @@ +package net.coderbot.iris.gl.uniform; + +@FunctionalInterface +public interface FloatSupplier { + float getAsFloat(); +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/FloatUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/FloatUniform.java new file mode 100644 index 000000000..4de68e710 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/FloatUniform.java @@ -0,0 +1,38 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +public class FloatUniform extends Uniform { + private float cachedValue; + private final FloatSupplier value; + + FloatUniform(int location, FloatSupplier value) { + this(location, value, null); + } + + FloatUniform(int location, FloatSupplier value, ValueUpdateNotifier notifier) { + super(location, notifier); + + this.cachedValue = 0; + this.value = value; + } + + @Override + public void update() { + updateValue(); + + if (notifier != null) { + notifier.setListener(this::updateValue); + } + } + + private void updateValue() { + float newValue = value.getAsFloat(); + + if (cachedValue != newValue) { + cachedValue = newValue; + IrisRenderSystem.uniform1f(location, newValue); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/IntUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/IntUniform.java new file mode 100644 index 000000000..2d519efcf --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/IntUniform.java @@ -0,0 +1,40 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +import java.util.function.IntSupplier; + +public class IntUniform extends Uniform { + private int cachedValue; + private final IntSupplier value; + + IntUniform(int location, IntSupplier value) { + this(location, value, null); + } + + IntUniform(int location, IntSupplier value, ValueUpdateNotifier notifier) { + super(location, notifier); + + this.cachedValue = 0; + this.value = value; + } + + @Override + public void update() { + updateValue(); + + if (notifier != null) { + notifier.setListener(this::updateValue); + } + } + + private void updateValue() { + int newValue = value.getAsInt(); + + if (cachedValue != newValue) { + cachedValue = newValue; + IrisRenderSystem.uniform1i(location, newValue); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/LocationalUniformHolder.java b/src/main/java/net/coderbot/iris/gl/uniform/LocationalUniformHolder.java new file mode 100644 index 000000000..a0afb3f50 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/LocationalUniformHolder.java @@ -0,0 +1,119 @@ +package net.coderbot.iris.gl.uniform; + +import net.minecraft.util.Vec3; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import java.util.OptionalInt; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public interface LocationalUniformHolder extends UniformHolder { + LocationalUniformHolder addUniform(UniformUpdateFrequency updateFrequency, Uniform uniform); + + OptionalInt location(String name, UniformType type); + + @Override + default LocationalUniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, FloatSupplier value) { + location(name, UniformType.FLOAT).ifPresent(id -> addUniform(updateFrequency, new FloatUniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, IntSupplier value) { + location(name, UniformType.FLOAT).ifPresent(id -> addUniform(updateFrequency, new FloatUniform(id, () -> (float) value.getAsInt()))); + + return this; + } + + @Override + default LocationalUniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, DoubleSupplier value) { + location(name, UniformType.FLOAT).ifPresent(id -> addUniform(updateFrequency, new FloatUniform(id, () -> (float) value.getAsDouble()))); + + return this; + } + + @Override + default LocationalUniformHolder uniform1i(UniformUpdateFrequency updateFrequency, String name, IntSupplier value) { + location(name, UniformType.INT).ifPresent(id -> addUniform(updateFrequency, new IntUniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform1b(UniformUpdateFrequency updateFrequency, String name, BooleanSupplier value) { + location(name, UniformType.INT).ifPresent(id -> addUniform(updateFrequency, new BooleanUniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform2f(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC2).ifPresent(id -> addUniform(updateFrequency, new Vector2Uniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform2i(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC2I).ifPresent(id -> addUniform(updateFrequency, new Vector2IntegerJomlUniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform3f(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC3).ifPresent(id -> addUniform(updateFrequency, new Vector3Uniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniformVanilla3f(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC3).ifPresent(id -> addUniform(updateFrequency, new VanillaVector3Uniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniformTruncated3f(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC3).ifPresent(id -> addUniform(updateFrequency, Vector3Uniform.truncated(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform3d(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC3).ifPresent(id -> addUniform(updateFrequency, Vector3Uniform.converted(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniform4f(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.VEC4).ifPresent(id -> addUniform(updateFrequency, new Vector4Uniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniformMatrix(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.MAT4).ifPresent(id -> addUniform(updateFrequency, new MatrixUniform(id, value))); + + return this; + } + + @Override + default LocationalUniformHolder uniformMatrixFromArray(UniformUpdateFrequency updateFrequency, String name, Supplier value) { + location(name, UniformType.MAT4).ifPresent(id -> addUniform(updateFrequency, new MatrixFromFloatArrayUniform(id, value))); + + return this; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/MatrixFromFloatArrayUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/MatrixFromFloatArrayUniform.java new file mode 100644 index 000000000..b4a895270 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/MatrixFromFloatArrayUniform.java @@ -0,0 +1,35 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; +import java.util.Arrays; +import java.util.function.Supplier; + +public class MatrixFromFloatArrayUniform extends Uniform { + private final FloatBuffer buffer = BufferUtils.createFloatBuffer(16); + private float[] cachedValue; + private final Supplier value; + + MatrixFromFloatArrayUniform(int location, Supplier value) { + super(location); + + this.cachedValue = null; + this.value = value; + } + + @Override + public void update() { + float[] newValue = value.get(); + + if (!Arrays.equals(newValue, cachedValue)) { + cachedValue = Arrays.copyOf(newValue, 16); + + buffer.put(cachedValue); + buffer.rewind(); + + IrisRenderSystem.uniformMatrix4fv(location, false, buffer); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/MatrixUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/MatrixUniform.java new file mode 100644 index 000000000..8b7d84fed --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/MatrixUniform.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; +import java.util.function.Supplier; + +public class MatrixUniform extends Uniform { + private final FloatBuffer buffer = BufferUtils.createFloatBuffer(16); + private Matrix4f cachedValue; + private final Supplier value; + + MatrixUniform(int location, Supplier value) { + super(location); + + this.cachedValue = null; + this.value = value; + } + + @Override + public void update() { + final Matrix4f newValue = value.get(); + if( newValue == null ){ + throw new RuntimeException("MatrixUniform value is null"); + } + if (!newValue.equals(cachedValue)) { + cachedValue = new Matrix4f(newValue); + + cachedValue.get(buffer); + buffer.rewind(); + + IrisRenderSystem.uniformMatrix4fv(location, false, buffer); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Uniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Uniform.java new file mode 100644 index 000000000..520b27890 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Uniform.java @@ -0,0 +1,27 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +public abstract class Uniform { + protected final int location; + protected final ValueUpdateNotifier notifier; + + Uniform(int location) { + this(location, null); + } + + Uniform(int location, ValueUpdateNotifier notifier) { + this.location = location; + this.notifier = notifier; + } + + public abstract void update(); + + public final int getLocation() { + return location; + } + + public final ValueUpdateNotifier getNotifier() { + return notifier; + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/UniformHolder.java b/src/main/java/net/coderbot/iris/gl/uniform/UniformHolder.java new file mode 100644 index 000000000..05f339a09 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/UniformHolder.java @@ -0,0 +1,46 @@ +package net.coderbot.iris.gl.uniform; + +import net.minecraft.util.Vec3; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public interface UniformHolder { + UniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, FloatSupplier value); + + UniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, IntSupplier value); + + UniformHolder uniform1f(UniformUpdateFrequency updateFrequency, String name, DoubleSupplier value); + + UniformHolder uniform1i(UniformUpdateFrequency updateFrequency, String name, IntSupplier value); + + UniformHolder uniform1b(UniformUpdateFrequency updateFrequency, String name, BooleanSupplier value); + + UniformHolder uniform2f(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniform2i(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniform3f(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniformVanilla3f(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniformTruncated3f(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniform3d(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniform4f(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniformMatrix(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder uniformMatrixFromArray(UniformUpdateFrequency updateFrequency, String name, Supplier value); + + UniformHolder externallyManagedUniform(String name, UniformType type); +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/UniformType.java b/src/main/java/net/coderbot/iris/gl/uniform/UniformType.java new file mode 100644 index 000000000..0bc4f20fd --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/UniformType.java @@ -0,0 +1,12 @@ +package net.coderbot.iris.gl.uniform; + +public enum UniformType { + INT, + FLOAT, + MAT4, + VEC2, + VEC2I, + VEC3, + VEC4, + VEC4I +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/UniformUpdateFrequency.java b/src/main/java/net/coderbot/iris/gl/uniform/UniformUpdateFrequency.java new file mode 100644 index 000000000..e6b76acce --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/UniformUpdateFrequency.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.gl.uniform; + +public enum UniformUpdateFrequency { + ONCE, + PER_TICK, + PER_FRAME +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/VanillaVector3Uniform.java b/src/main/java/net/coderbot/iris/gl/uniform/VanillaVector3Uniform.java new file mode 100644 index 000000000..172e6bdda --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/VanillaVector3Uniform.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.minecraft.util.Vec3; + +import java.util.function.Supplier; + +public class VanillaVector3Uniform extends Uniform { + private final Vec3 cachedValue; + private final Supplier value; + + VanillaVector3Uniform(int location, Supplier value) { + super(location); + + this.cachedValue = Vec3.createVectorHelper(0, 0, 0); + this.value = value; + } + + @Override + public void update() { + Vec3 newValue = value.get(); + + if (!newValue.equals(cachedValue)) { + cachedValue.xCoord = newValue.xCoord; + cachedValue.yCoord = newValue.yCoord; + cachedValue.zCoord = newValue.zCoord; + IrisRenderSystem.uniform3f(location, (float)cachedValue.xCoord, (float)cachedValue.yCoord, (float)cachedValue.zCoord); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Vector2IntegerJomlUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Vector2IntegerJomlUniform.java new file mode 100644 index 000000000..785ef87d4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Vector2IntegerJomlUniform.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import org.joml.Vector2i; + +import java.util.function.Supplier; + +public class Vector2IntegerJomlUniform extends Uniform { + private Vector2i cachedValue; + private final Supplier value; + + Vector2IntegerJomlUniform(int location, Supplier value) { + this(location, value, null); + } + + Vector2IntegerJomlUniform(int location, Supplier value, ValueUpdateNotifier notifier) { + super(location, notifier); + + this.cachedValue = null; + this.value = value; + } + + @Override + public void update() { + updateValue(); + + if (notifier != null) { + notifier.setListener(this::updateValue); + } + } + + private void updateValue() { + Vector2i newValue = value.get(); + + if (cachedValue == null || !newValue.equals(cachedValue)) { + cachedValue = newValue; + IrisRenderSystem.uniform2i(this.location, newValue.x, newValue.y); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Vector2Uniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Vector2Uniform.java new file mode 100644 index 000000000..379b33512 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Vector2Uniform.java @@ -0,0 +1,29 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.joml.Vector2f; + +import java.util.function.Supplier; + +public class Vector2Uniform extends Uniform { + private Vector2f cachedValue; + private final Supplier value; + + Vector2Uniform(int location, Supplier value) { + super(location); + + this.cachedValue = null; + this.value = value; + + } + + @Override + public void update() { + Vector2f newValue = value.get(); + + if (cachedValue == null || !newValue.equals(cachedValue)) { + cachedValue = newValue; + IrisRenderSystem.uniform2f(this.location, newValue.x, newValue.y); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Vector3Uniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Vector3Uniform.java new file mode 100644 index 000000000..3475b0785 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Vector3Uniform.java @@ -0,0 +1,54 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import java.util.function.Supplier; + +public class Vector3Uniform extends Uniform { + private final Vector3f cachedValue; + private final Supplier value; + + Vector3Uniform(int location, Supplier value) { + super(location); + + this.cachedValue = new Vector3f(); + this.value = value; + } + + static Vector3Uniform converted(int location, Supplier value) { + Vector3f held = new Vector3f(); + + return new Vector3Uniform(location, () -> { + Vector3d updated = value.get(); + + held.set((float) updated.x, (float) updated.y, (float) updated.z); + + return held; + }); + } + + static Vector3Uniform truncated(int location, Supplier value) { + Vector3f held = new Vector3f(); + + return new Vector3Uniform(location, () -> { + Vector4f updated = value.get(); + + held.set(updated.x(), updated.y(), updated.z()); + + return held; + }); + } + + @Override + public void update() { + Vector3f newValue = value.get(); + + if (!newValue.equals(cachedValue)) { + cachedValue.set(newValue.x(), newValue.y(), newValue.z()); + IrisRenderSystem.uniform3f(location, cachedValue.x(), cachedValue.y(), cachedValue.z()); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Vector4IntegerJomlUniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Vector4IntegerJomlUniform.java new file mode 100644 index 000000000..b373fa766 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Vector4IntegerJomlUniform.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import org.joml.Vector4i; + +import java.util.function.Supplier; + +public class Vector4IntegerJomlUniform extends Uniform { + private Vector4i cachedValue; + private final Supplier value; + + Vector4IntegerJomlUniform(int location, Supplier value) { + this(location, value, null); + } + + Vector4IntegerJomlUniform(int location, Supplier value, ValueUpdateNotifier notifier) { + super(location, notifier); + + this.cachedValue = null; + this.value = value; + } + + @Override + public void update() { + updateValue(); + + if (notifier != null) { + notifier.setListener(this::updateValue); + } + } + + private void updateValue() { + Vector4i newValue = value.get(); + + if (cachedValue == null || !newValue.equals(cachedValue)) { + cachedValue = newValue; + IrisRenderSystem.uniform4i(this.location, newValue.x, newValue.y, newValue.z, newValue.w); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gl/uniform/Vector4Uniform.java b/src/main/java/net/coderbot/iris/gl/uniform/Vector4Uniform.java new file mode 100644 index 000000000..41a9d61f3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gl/uniform/Vector4Uniform.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.gl.uniform; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import org.joml.Vector4f; + +import java.util.function.Supplier; + +public class Vector4Uniform extends Uniform { + private final Vector4f cachedValue; + private final Supplier value; + + Vector4Uniform(int location, Supplier value) { + this(location, value, null); + } + + Vector4Uniform(int location, Supplier value, ValueUpdateNotifier notifier) { + super(location, notifier); + + this.cachedValue = new Vector4f(); + this.value = value; + } + + @Override + public void update() { + updateValue(); + + if (notifier != null) { + notifier.setListener(this::updateValue); + } + } + + private void updateValue() { + Vector4f newValue = value.get(); + + if (!newValue.equals(cachedValue)) { + cachedValue.set(newValue.x(), newValue.y(), newValue.z(), newValue.w()); + IrisRenderSystem.uniform4f(location, cachedValue.x(), cachedValue.y(), cachedValue.z(), cachedValue.w()); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gui/FileDialogUtil.java b/src/main/java/net/coderbot/iris/gui/FileDialogUtil.java new file mode 100644 index 000000000..51f67a821 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/FileDialogUtil.java @@ -0,0 +1,63 @@ +package net.coderbot.iris.gui; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Class used to make interfacing with {@link TinyFileDialogs} easier and asynchronous. + */ +public final class FileDialogUtil { + private static final ExecutorService FILE_DIALOG_EXECUTOR = Executors.newSingleThreadExecutor(); + + private FileDialogUtil() {} + + /** + * Opens an asynchronous file select dialog window. + * + * @param dialog Whether to open a "save" dialog or an "open" dialog + * @param title The title of the dialog window + * @param origin The path that the window should start at + * @param filterLabel A label used to describe what file extensions are allowed and their purpose + * @param filters The file extension filters used by the dialog, each formatted as {@code "*.extension"} + * @return a {@link CompletableFuture} which is completed once a file is selected or the dialog is cancelled. + */ +// public static CompletableFuture> fileSelectDialog(DialogType dialog, String title, @Nullable Path origin, @Nullable String filterLabel, String ... filters) { +// CompletableFuture> future = new CompletableFuture<>(); +// +// FILE_DIALOG_EXECUTOR.submit(() -> { +// String result = null; +// +// try (MemoryStack stack = MemoryStack.stackPush()) { +// PointerBuffer filterBuffer = stack.mallocPointer(filters.length); +// +// for (String filter : filters) { +// filterBuffer.put(stack.UTF8(filter)); +// } +// filterBuffer.flip(); +// +// String path = origin != null ? origin.toAbsolutePath().toString() : null; +// +// if (dialog == DialogType.SAVE) { +// result = TinyFileDialogs.tinyfd_saveFileDialog(title, path, filterBuffer, filterLabel); +// } else if (dialog == DialogType.OPEN) { +// result = TinyFileDialogs.tinyfd_openFileDialog(title, path, filterBuffer, filterLabel, false); +// } +// } +// +// future.complete(Optional.ofNullable(result).map(Paths::get)); +// }); +// +// return future; +// } + + public enum DialogType { + SAVE, OPEN + } +} diff --git a/src/main/java/net/coderbot/iris/gui/GuiUtil.java b/src/main/java/net/coderbot/iris/gui/GuiUtil.java new file mode 100644 index 000000000..66e6fdfd4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/GuiUtil.java @@ -0,0 +1,208 @@ +package net.coderbot.iris.gui; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import cpw.mods.fml.client.config.GuiUtils; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL14; + +/** + * Class serving as abstraction and + * centralization for common GUI + * rendering/other code calls. + * + * Helps allow for easier portability + * to Minecraft 1.17 by abstracting + * some code that will be changed. + */ +public final class GuiUtil { + public static final ResourceLocation IRIS_WIDGETS_TEX = new ResourceLocation("iris", "textures/gui/widgets.png"); + private static final String ELLIPSIS = "..."; + + private GuiUtil() {} + + private static Minecraft client() { + return Minecraft.getMinecraft(); + } + + /** + * Binds Iris's widgets texture to be + * used for succeeding draw calls. + */ + public static void bindIrisWidgetsTexture() { + client().getTextureManager().bindTexture(IRIS_WIDGETS_TEX); + } + + /** + * Draws a button. Button textures must be mapped with the + * same coordinates as those on the vanilla widgets texture. + * + * @param x X position of the left of the button + * @param y Y position of the top of the button + * @param width Width of the button, maximum 398 + * @param height Height of the button, maximum 20 + * @param hovered Whether the button is being hovered over with the mouse + * @param disabled Whether the button should use the "disabled" texture + */ + public static void drawButton(int x, int y, int width, int height, boolean hovered, boolean disabled) { + // Create variables for half of the width and height. + // Will not be exact when width and height are odd, but + // that case is handled within the draw calls. + final int halfWidth = width / 2; + final int halfHeight = height / 2; + + // V offset for which button texture to use + final int vOffset = disabled ? 46 : hovered ? 86 : 66; + + // Sets RenderSystem to use solid white as the tint color for blend mode, and enables blend mode + GL14.glBlendColor(1.0f, 1.0f, 1.0f, 1.0f); + GLStateManager.enableBlend(); + + // Sets RenderSystem to be able to use textures when drawing + GLStateManager.enableTexture(); + + // Top left section + GuiUtils.drawTexturedModalRect(x, y, 0, vOffset, halfWidth, halfHeight, 0); + // Top right section + GuiUtils.drawTexturedModalRect(x + halfWidth, y, 200 - (width - halfWidth), vOffset, width - halfWidth, halfHeight, 0); + // Bottom left section + GuiUtils.drawTexturedModalRect(x, y + halfHeight, 0, vOffset + (20 - (height - halfHeight)), halfWidth, height - halfHeight, 0); + // Bottom right section + GuiUtils.drawTexturedModalRect(x + halfWidth, y + halfHeight, 200 - (width - halfWidth), vOffset + (20 - (height - halfHeight)), width - halfWidth, height - halfHeight, 0); + } + + /** + * Draws a translucent black panel + * with a light border. + * + * @param x The x position of the panel + * @param y The y position of the panel + * @param width The width of the panel + * @param height The height of the panel + */ + public static void drawPanel(int x, int y, int width, int height) { + final int borderColor = 0xDEDEDEDE; + final int innerColor = 0xDE000000; + + // Top border section + Gui.drawRect(x, y, x + width, y + 1, borderColor); + // Bottom border section + Gui.drawRect(x, (y + height) - 1, x + width, y + height, borderColor); + // Left border section + Gui.drawRect(x, y + 1, x + 1, (y + height) - 1, borderColor); + // Right border section + Gui.drawRect((x + width) - 1, y + 1, x + width, (y + height) - 1, borderColor); + // Inner section + Gui.drawRect(x + 1, y + 1, (x + width) - 1, (y + height) - 1, innerColor); + } + + /** + * Draws a text with a panel behind it. + * + * @param text The text String to draw + * @param x The x position of the panel + * @param y The y position of the panel + */ + public static void drawTextPanel(FontRenderer font, String text, int x, int y) { + drawPanel(x, y, font.getStringWidth(text) + 8, 16); + font.drawStringWithShadow(text, x + 4, y + 4, 0xFFFFFF); + } + + /** + * Shorten a text to a specific length, adding an ellipsis (...) + * to the end if shortened. + * + * Text may lose formatting. + * + * @param font Font to use for determining the width of text + * @param text Text to shorten + * @param width Width to shorten text to + * @return a shortened text + */ + public static String shortenText(FontRenderer font, String text, int width) { + if (font.getStringWidth(text) > width) { + return font.trimStringToWidth(text, width - font.getStringWidth(ELLIPSIS)) + ELLIPSIS; + } + return text; + } + + /** + * Creates a new translated text, if a translation + * is present. If not, will return the default text + * String passed. + * + * @param defaultText Default text to use if no translation is found + * @param translationDesc Translation key to try and use + * @param format Formatting arguments for the translated text, if created + * @return the translated text if found, otherwise the default provided + */ + public static String translateOrDefault(String defaultText, String translationDesc, Object ... format) { + final String translated = I18n.format(translationDesc, format); + if(!translated.equals(translationDesc)) { + return translated; + } + return defaultText; + } + + /** + * Plays the {@code UI_BUTTON_CLICK} sound event as a + * master sound effect. + * + * Used in non-{@code ButtonWidget} UI elements upon click + * or other action. + */ + public static void playButtonClickSound() { +// client().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1)); + } + + /** + * A class representing a section of a + * texture, to be easily drawn in GUIs. + */ + public static class Icon { + public static final Icon SEARCH = new Icon(0, 0, 7, 8); + public static final Icon CLOSE = new Icon(7, 0, 5, 6); + public static final Icon REFRESH = new Icon(12, 0, 10, 10); + public static final Icon EXPORT = new Icon(22, 0, 7, 8); + public static final Icon EXPORT_COLORED = new Icon(29, 0, 7, 8); + public static final Icon IMPORT = new Icon(22, 8, 7, 8); + public static final Icon IMPORT_COLORED = new Icon(29, 8, 7, 8); + + private final int u; + private final int v; + @Getter + private final int width; + @Getter + private final int height; + + public Icon(int u, int v, int width, int height) { + this.u = u; + this.v = v; + this.width = width; + this.height = height; + } + + /** + * Draws this icon to the screen at the specified coordinates. + * + * @param x The x position to draw the icon at (left) + * @param y The y position to draw the icon at (top) + */ + public void draw(int x, int y) { + // Sets RenderSystem to use solid white as the tint color for blend mode, and enables blend mode + GL14.glBlendColor(1.0f, 1.0f, 1.0f, 1.0f); + GLStateManager.enableBlend(); + + // Sets RenderSystem to be able to use textures when drawing + GLStateManager.enableTexture(); + + // Draw the texture to the screen + GuiUtils.drawTexturedModalRect(x, y, u, v, width, height, 256); + } + + } +} diff --git a/src/main/java/net/coderbot/iris/gui/NavigationController.java b/src/main/java/net/coderbot/iris/gui/NavigationController.java new file mode 100644 index 000000000..bb4d183f8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/NavigationController.java @@ -0,0 +1,65 @@ +package net.coderbot.iris.gui; + +import lombok.Getter; +import net.coderbot.iris.gui.element.ShaderPackOptionList; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class NavigationController { + private final OptionMenuContainer container; + private ShaderPackOptionList optionList; + + @Getter + private String currentScreen = null; + private final Deque history = new ArrayDeque<>(); + + public NavigationController(OptionMenuContainer container) { + this.container = container; + } + + public void back() { + if (history.size() > 0) { + history.removeLast(); + + if (history.size() > 0) { + currentScreen = history.getLast(); + } else { + currentScreen = null; + } + } else { + currentScreen = null; + } + + this.rebuild(); + } + + public void open(String screen) { + currentScreen = screen; + history.addLast(screen); + + this.rebuild(); + } + + public void rebuild() { + if (optionList != null) { + optionList.rebuild(); + } + } + + public void refresh() { + if (optionList != null) { + optionList.refresh(); + } + } + + public boolean hasHistory() { + return this.history.size() > 0; + } + + public void setActiveOptionList(ShaderPackOptionList optionList) { + this.optionList = optionList; + } + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/IrisElementRow.java b/src/main/java/net/coderbot/iris/gui/element/IrisElementRow.java new file mode 100644 index 000000000..ea94291d0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/IrisElementRow.java @@ -0,0 +1,223 @@ +package net.coderbot.iris.gui.element; + +import lombok.Getter; +import net.coderbot.iris.gui.GuiUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * Intended to make very simple rows of buttons easier to make + */ +public class IrisElementRow { + private final Map elements = new HashMap<>(); + private final List orderedElements = new ArrayList<>(); + private final int spacing; + private int x; + private int y; + private int width; + private int height; + + public IrisElementRow(int spacing) { + this.spacing = spacing; + } + + public IrisElementRow() { + this(1); + } + + /** + * Adds an element to the right of this row. + * + * @param element The element to add + * @param width The width of the element in this row + * @return {@code this}, to be used for chaining statements + */ + public IrisElementRow add(Element element, int width) { + if (!this.orderedElements.contains(element)) { + this.orderedElements.add(element); + } + this.elements.put(element, width); + + this.width += width + this.spacing; + + return this; + } + + /** + * Modifies the width of an element. + * + * @param element The element whose width to modify + * @param width The width to be assigned to the specified element + */ + public void setWidth(Element element, int width) { + if (!this.elements.containsKey(element)) { + return; + } + + this.width -= this.elements.get(element) + 2; + + add(element, width); + } + + /** + * Renders the row, with the anchor point being the top left. + */ + public void drawScreen(int x, int y, int height, int mouseX, int mouseY, float tickDelta, boolean rowHovered) { + this.x = x; + this.y = y; + this.height = height; + + int currentX = x; + + for (Element element : this.orderedElements) { + final int currentWidth = this.elements.get(element); + + element.render(currentX, y, currentWidth, height, mouseX, mouseY, tickDelta, + rowHovered && sectionHovered(currentX, currentWidth, mouseX, mouseY)); + + currentX += currentWidth + this.spacing; + } + } + + /** + * Renders the row, with the anchor point being the top right. + */ + public void renderRightAligned(int x, int y, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + drawScreen(x - this.width, y, height, mouseX, mouseY, tickDelta, hovered); + } + + private boolean sectionHovered(int sectionX, int sectionWidth, double mx, double my) { + return mx > sectionX && mx < sectionX + sectionWidth && + my > this.y && my < this.y + this.height; + } + + private Optional getHovered(double mx, double my) { + int currentX = this.x; + + for (Element element : this.orderedElements) { + final int currentWidth = this.elements.get(element); + + if (sectionHovered(currentX, currentWidth, mx, my)) { + return Optional.of(element); + } + + currentX += currentWidth + this.spacing; + } + + return Optional.empty(); + } + + public boolean mouseClicked(double mx, double my, int button) { + return getHovered(mx, my).map(element -> element.mouseClicked(mx, my, button)).orElse(false); + } + + public boolean mouseReleased(double mx, double my, int button) { + return getHovered(mx, my).map(element -> element.mouseReleased(mx, my, button)).orElse(false); + } + + public abstract static class Element { + public boolean disabled = false; + @Getter private boolean hovered = false; + + public void render(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + GuiUtil.bindIrisWidgetsTexture(); + GuiUtil.drawButton(x, y, width, height, hovered, this.disabled); + + this.hovered = hovered; + this.renderLabel(x, y, width, height, mouseX, mouseY, tickDelta, hovered); + } + + public abstract void renderLabel(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered); + + public boolean mouseClicked(double mx, double my, int button) { + return false; + } + + public boolean mouseReleased(double mx, double my, int button) { + return false; + } + + } + + public abstract static class ButtonElement> extends Element { + private final Function onClick; + + protected ButtonElement(Function onClick) { + this.onClick = onClick; + } + + @Override + public boolean mouseClicked(double mx, double my, int button) { + if (this.disabled) { + return false; + } + + if (button == 0) { + return this.onClick.apply((T) this); + } + + return super.mouseClicked(mx, my, button); + } + } + + /** + * A clickable button element that uses a {@link net.coderbot.iris.gui.GuiUtil.Icon} as its label. + */ + public static class IconButtonElement extends ButtonElement { + public GuiUtil.Icon icon; + public GuiUtil.Icon hoveredIcon; + + public IconButtonElement(GuiUtil.Icon icon, GuiUtil.Icon hoveredIcon, Function onClick) { + super(onClick); + this.icon = icon; + this.hoveredIcon = hoveredIcon; + } + + public IconButtonElement(GuiUtil.Icon icon, Function onClick) { + this(icon, icon, onClick); + } + + @Override + public void renderLabel(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + final int iconX = x + (int)((width - this.icon.getWidth()) * 0.5); + final int iconY = y + (int)((height - this.icon.getHeight()) * 0.5); + + GuiUtil.bindIrisWidgetsTexture(); + if (!this.disabled && hovered) { + this.hoveredIcon.draw(iconX, iconY); + } else { + this.icon.draw(iconX, iconY); + } + } + } + + /** + * A clickable button element that uses a text component as its label. + */ + public static class TextButtonElement extends ButtonElement { + protected final FontRenderer font; + public String text; + + public TextButtonElement(String text, Function onClick) { + super(onClick); + + this.font = Minecraft.getMinecraft().fontRenderer; + this.text = text; + } + + @Override + public void renderLabel(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + final int textX = x + (int)((width - this.font.getStringWidth(this.text)) * 0.5); + final int textY = y + (int)((height - 8) * 0.5); + + this.font.drawStringWithShadow(this.text, textX, textY, 0xFFFFFF); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/IrisGuiSlot.java b/src/main/java/net/coderbot/iris/gui/element/IrisGuiSlot.java new file mode 100644 index 000000000..26b5b4379 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/IrisGuiSlot.java @@ -0,0 +1,214 @@ +package net.coderbot.iris.gui.element; + +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiSlot; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +// TODO: look into GuiListExtended & GuiSelectStringEntries +public abstract class IrisGuiSlot extends GuiSlot { + @Setter @Getter protected boolean renderBackground = true; + boolean scrolling = false; + + protected IrisGuiSlot(Minecraft mc, int width, int height, int top, int bottom, int slotHeight) { + super(mc, width, height, top, bottom, slotHeight); + // Set Center Vertically to false + this.field_148163_i = false; + + } + + @Override + protected void drawContainerBackground(Tessellator tessellator) { + if (this.renderBackground) { + super.drawContainerBackground(tessellator); + } + } + + @Override + protected int getScrollBarX() { + // Position the scrollbar at the rightmost edge of the screen. + // By default, the scrollbar is positioned moderately offset from the center. + return this.width - 6; + } + + @Override + protected void drawSelectionBox(int x, int y, int mouseX, int mouseY) { + final int oldPadding = this.headerPadding; + this.headerPadding = 2; + super.drawSelectionBox(x, y, mouseX, mouseY); + this.headerPadding = oldPadding; + } + + @Override + protected void elementClicked(int index, boolean doubleClick, int mouseX, int mouseY) { + // Do nothing + } + + protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int button) { + return false; + } + + public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) { + if (!this.func_148125_i/*enabled*/()) { + return false; + } + final int size = this.getSize(); + final int scrollBarX = this.getScrollBarX(); + final int rightEdge = scrollBarX + 6; + final int elementLeft = this.width / 2 - this.getListWidth() / 2; + final int elementRight = this.width / 2 + this.getListWidth() / 2; + final int relativeY = mouseY - this.top - this.headerPadding + (int) this.amountScrolled - 4; + boolean handled = false; + final boolean leftMouseDown = Mouse.isButtonDown(0); + final boolean rightMouseDown = Mouse.isButtonDown(1); + + if (mouseX <= this.left || mouseX >= this.right || mouseY <= this.top || mouseY >= this.bottom) { + return handled; + } + if (leftMouseDown && mouseX >= scrollBarX && mouseX <= rightEdge) { + scrolling = true; + this.initialClickY = (float) mouseY; + } else if ((leftMouseDown || rightMouseDown)) { + final int index = relativeY / this.slotHeight; + + if (mouseX >= elementLeft && mouseX <= elementRight && index >= 0 && relativeY >= 0 && index < size) { + final boolean doubleCLick = index == this.selectedElement && Minecraft.getSystemTime() - this.lastClicked < 250L; + + handled = this.elementClicked(index, doubleCLick, mouseX, mouseY, mouseButton); + this.selectedElement = index; + this.lastClicked = Minecraft.getSystemTime(); + } else if (mouseX >= elementLeft && mouseX <= elementRight && relativeY < 0) { + this.func_148132_a(mouseX - elementLeft, mouseY - this.top + (int) this.amountScrolled - 4); + } + } + + return handled; + } + + public boolean mouseReleased(int mouseX, int mouseY, int button) { + scrolling = false; + return false; + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.mouseX = mouseX; + this.mouseY = mouseY; + this.drawBackground(); + final int scrollBarX = this.getScrollBarX(); + final int rightEdge = scrollBarX + 6; + final byte offset = 4; + + + // Scrollbar nonsense + if (scrolling) { + this.amountScrolled += ((float) mouseY - this.initialClickY); + this.initialClickY = mouseY; + } else { + for (; !this.mc.gameSettings.touchscreen && Mouse.next(); this.mc.currentScreen.handleMouseInput()) { + int dWheel = Mouse.getEventDWheel(); + + if (dWheel != 0) { + if (dWheel > 0) { + dWheel = -1; + } else { + dWheel = 1; + } + + this.amountScrolled += (dWheel * this.slotHeight / 2.0f); + } + } + } + + + this.bindAmountScrolled(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_FOG); + final Tessellator tessellator = Tessellator.instance; + drawContainerBackground(tessellator); + final int elementRight = this.left + this.width / 2 - this.getListWidth() / 2 + 2; + final int relativeY = this.top + 4 - (int) this.amountScrolled; + + if (this.hasListHeader) { + this.drawListHeader(elementRight, relativeY, tessellator); + } + + this.drawSelectionBox(elementRight, relativeY, mouseX, mouseY); + GL11.glDisable(GL11.GL_DEPTH_TEST); + this.overlayBackground(0, this.top, 255, 255); + this.overlayBackground(this.bottom, this.height, 255, 255); + GL11.glEnable(GL11.GL_BLEND); + OpenGlHelper.glBlendFunc(770, 771, 0, 1); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glDisable(GL11.GL_TEXTURE_2D); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 0); + tessellator.addVertexWithUV(this.left, (this.top + offset), 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(this.right, (this.top + offset), 0.0D, 1.0D, 1.0D); + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(this.right, this.top, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(this.left, this.top, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(this.left, this.bottom, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(this.right, this.bottom, 0.0D, 1.0D, 1.0D); + tessellator.setColorRGBA_I(0, 0); + tessellator.addVertexWithUV(this.right, (this.bottom - offset), 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(this.left, (this.bottom - offset), 0.0D, 0.0D, 0.0D); + tessellator.draw(); + + // Draw scrollbar if needed + final int contentOverflow = this.func_148135_f(); + if (contentOverflow > 0) { + registerScrollButtons(7, 8); + int scrollPosSize = (this.bottom - this.top) * (this.bottom - this.top) / this.getContentHeight(); + + if (scrollPosSize < 32) { + scrollPosSize = 32; + } + + if (scrollPosSize > this.bottom - this.top - 8) { + scrollPosSize = this.bottom - this.top - 8; + } + + int scrollPos = (int) this.amountScrolled * (this.bottom - this.top - scrollPosSize) / contentOverflow + this.top; + + if (scrollPos < this.top) { + scrollPos = this.top; + } + + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(scrollBarX, this.bottom, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(rightEdge, this.bottom, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(rightEdge, this.top, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarX, this.top, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(8421504, 255); + tessellator.addVertexWithUV(scrollBarX, (scrollPos + scrollPosSize), 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(rightEdge, (scrollPos + scrollPosSize), 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(rightEdge, scrollPos, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarX, scrollPos, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(12632256, 255); + tessellator.addVertexWithUV(scrollBarX, (scrollPos + scrollPosSize - 1), 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV((rightEdge - 1), (scrollPos + scrollPosSize - 1), 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV((rightEdge - 1), scrollPos, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarX, scrollPos, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + } + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glDisable(GL11.GL_BLEND); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java b/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java new file mode 100644 index 000000000..a3ec7fea3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java @@ -0,0 +1,114 @@ +package net.coderbot.iris.gui.element; + +import lombok.Getter; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.element.shaderoptions.BaseEntry; +import net.coderbot.iris.gui.element.shaderoptions.ElementRowEntry; +import net.coderbot.iris.gui.element.shaderoptions.HeaderEntry; +import net.coderbot.iris.gui.element.widget.AbstractElementWidget; +import net.coderbot.iris.gui.element.widget.OptionMenuConstructor; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.ShaderPack; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Tessellator; + +import java.util.ArrayList; +import java.util.List; + +public class ShaderPackOptionList extends IrisGuiSlot { + private final List> elementWidgets = new ArrayList<>(); + private final ShaderPackScreen screen; + @Getter + private final NavigationController navigation; + private OptionMenuContainer container; + private final List entries = new ArrayList<>(); + + public ShaderPackOptionList(ShaderPackScreen screen, NavigationController navigation, ShaderPack pack, Minecraft client, int width, int height, int top, int bottom, int left, int right) { + super(client, width, height, top, bottom, 20); + this.navigation = navigation; + this.screen = screen; + + applyShaderPack(pack); + } + + public void applyShaderPack(ShaderPack pack) { + this.container = pack.getMenuContainer(); + } + + public void rebuild() { + this.entries.clear(); + this.amountScrolled = 0; + OptionMenuConstructor.constructAndApplyToScreen(this.container, this.screen, this, navigation); + } + + public void refresh() { + this.elementWidgets.forEach(widget -> widget.init(this.screen, this.navigation)); + } + + @Override + public int getListWidth() { + return Math.min(400, width - 12); + } + + protected void addEntry(BaseEntry entry) { + this.entries.add(entry); + } + + public void addHeader(String text, boolean backButton) { + this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton)); + } + + public void addWidgets(int columns, List> elements) { + this.elementWidgets.addAll(elements); + + List> row = new ArrayList<>(); + for (AbstractElementWidget element : elements) { + row.add(element); + + if (row.size() >= columns) { + this.addEntry(new ElementRowEntry(screen, this.navigation, row)); + row = new ArrayList<>(); // Clearing the list would affect the row entry created above + } + } + + if (row.size() > 0) { + while (row.size() < columns) { + row.add(AbstractElementWidget.EMPTY); + } + + this.addEntry(new ElementRowEntry(screen, this.navigation, row)); + } + } + + @Override + protected int getSize() { + return entries.size(); + } + + @Override + protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int mouseButton) { + final BaseEntry entry = this.entries.get(index); + return entry.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + protected boolean isSelected(int idx) { + return false;//return this.entries.get(idx).equals(this.selected); + } + + @Override + protected void drawBackground() { + // noop + } + + @Override + protected void drawSlot(int index, int x, int y, int i1, Tessellator tessellator, int mouseX, int mouseY) { + final BaseEntry entry = this.entries.get(index); + final boolean isMouseOver = this.func_148124_c/*getSlotIndexFromScreenCoords*/(mouseX, mouseY) == index; + entry.drawEntry(screen, index, x - 2, y + 4, this.getListWidth(), this.slotHeight, tessellator, mouseX, mouseY, isMouseOver); + } + + + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java b/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java new file mode 100644 index 000000000..44156e420 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java @@ -0,0 +1,158 @@ +package net.coderbot.iris.gui.element; + +import lombok.Getter; +import lombok.Setter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.element.shaderselection.BaseEntry; +import net.coderbot.iris.gui.element.shaderselection.LabelEntry; +import net.coderbot.iris.gui.element.shaderselection.ShaderPackEntry; +import net.coderbot.iris.gui.element.shaderselection.TopButtonRowEntry; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Tessellator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ShaderPackSelectionList extends IrisGuiSlot { + @Getter + private final ShaderPackScreen screen; + @Getter + private final TopButtonRowEntry topButtonRow; + + @Setter + @Getter + private ShaderPackEntry applied = null; + + @Setter + @Getter + private ShaderPackEntry selected = null; + + private final List entries = new ArrayList<>(); + + public ShaderPackSelectionList(ShaderPackScreen screen, Minecraft client, int width, int height, int top, int bottom, int left, int right) { + super(client, width, height, top, bottom, 20); + + this.screen = screen; + this.topButtonRow = new TopButtonRowEntry(this, Iris.getIrisConfig().areShadersEnabled()); + + refresh(); + } + + public void refresh() { + this.entries.clear(); + + final Collection names; + + try { + names = Iris.getShaderpacksDirectoryManager().enumerate(); + } catch (Throwable e) { + Iris.logger.error("Error reading files while constructing selection UI", e); + + // Not translating this since it's going to be seen very rarely, + // We're just trying to get more information on a seemingly untraceable bug: + // - https://github.com/IrisShaders/Iris/issues/785 + this.addLabelEntries( + "", + "There was an error reading your shaderpacks directory", + "", + "Check your logs for more information.", + "Please file an issue report including a log file.", + "If you are able to identify the file causing this, please include it in your report as well.", + "Note that this might be an issue with folder permissions; ensure those are correct first." + ); + + return; + } + + this.entries.add(topButtonRow); + + // Only allow the enable/disable shaders button if the user has added a shader pack. Otherwise, the button will be disabled. + topButtonRow.allowEnableShadersButton = names.size() > 0; + + int index = 0; + + for (String name : names) { + index++; + addPackEntry(index, name); + } + + } + + public void addPackEntry(int index, String name) { + final ShaderPackEntry entry = new ShaderPackEntry(index, this, name); + + Iris.getIrisConfig().getShaderPackName().ifPresent(currentPackName -> { + if (name.equals(currentPackName)) { + setSelected(entry); + setApplied(entry); + } + }); + + this.entries.add(entry); + } + + public void addLabelEntries(String ... lines) { + for (String text : lines) { + this.entries.add(new LabelEntry(this, text)); + } + } + + public void select(String name) { + for (BaseEntry entry : this.entries) { + if (entry instanceof ShaderPackEntry shaderPackEntry && name.equals(shaderPackEntry.getPackName())) { + setSelected(shaderPackEntry); + return; + } + } + } + + @Override + protected int getSize() { + return this.entries.size(); + } + + @Override + protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int mouseButton) { + // Only do anything on left-click + if (mouseButton != 0) { + return false; + } + final BaseEntry entry = this.entries.get(index); + if(entry instanceof ShaderPackEntry shaderPackEntry) { + this.setSelected(shaderPackEntry); + if (!topButtonRow.shadersEnabled) { + topButtonRow.setShadersEnabled(true); + } + return true; + } else if( entry instanceof TopButtonRowEntry topButtonRowEntry) { + return topButtonRowEntry.mouseClicked(mouseX, mouseY, 0); + } + return false; + } + + @Override + protected boolean isSelected(int idx) { + return this.entries.get(idx).equals(this.selected); + } + + @Override + public int getListWidth() { + return Math.min(308, width - 50); + } + + @Override + protected void drawBackground() { + // Do nothing + } + + + @Override + protected void drawSlot(int index, int x, int y, int i1, Tessellator tessellator, int mouseX, int mouseY) { + final BaseEntry entry = this.entries.get(index); + final boolean isMouseOver = this.func_148124_c/*getSlotIndexFromScreenCoords*/(mouseX, mouseY) == index; + entry.drawEntry(screen, index, x - 2, y + 4, this.getListWidth(), tessellator, mouseX, mouseY, isMouseOver); + } + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/screen/ElementWidgetScreenData.java b/src/main/java/net/coderbot/iris/gui/element/screen/ElementWidgetScreenData.java new file mode 100644 index 000000000..d5f89f066 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/screen/ElementWidgetScreenData.java @@ -0,0 +1,15 @@ +package net.coderbot.iris.gui.element.screen; + + + +public class ElementWidgetScreenData { + public static final ElementWidgetScreenData EMPTY = new ElementWidgetScreenData("", true); + + public final String heading; + public final boolean backButton; + + public ElementWidgetScreenData(String heading, boolean backButton) { + this.heading = heading; + this.backButton = backButton; + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderoptions/BaseEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/BaseEntry.java new file mode 100644 index 000000000..b1bc897b9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/BaseEntry.java @@ -0,0 +1,18 @@ +package net.coderbot.iris.gui.element.shaderoptions; + +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.renderer.Tessellator; + +public abstract class BaseEntry { + protected final NavigationController navigation; + + protected BaseEntry(NavigationController navigation) { + this.navigation = navigation; + } + + public abstract boolean mouseClicked(int mouseX, int mouseY, int button); + + + public abstract void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver); +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderoptions/ElementRowEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/ElementRowEntry.java new file mode 100644 index 000000000..e75e69d0d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/ElementRowEntry.java @@ -0,0 +1,62 @@ +package net.coderbot.iris.gui.element.shaderoptions; + +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.element.widget.AbstractElementWidget; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.MathHelper; + +import java.util.List; + +public class ElementRowEntry extends BaseEntry { + + private final List> widgets; + private final ShaderPackScreen screen; + + private int cachedWidth; + private int cachedPosX; + + public ElementRowEntry(ShaderPackScreen screen, NavigationController navigation, List> widgets) { + super(navigation); + + this.screen = screen; + this.widgets = widgets; + } + + @Override + public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { + this.cachedWidth = slotWidth; + this.cachedPosX = x; + + // The amount of space widgets will occupy, excluding margins. Will be divided up between widgets. + int totalWidthWithoutMargins = slotWidth - (2 * (widgets.size() - 1)); + + totalWidthWithoutMargins -= 3; // Centers it for some reason + + // Width of a single widget + final float singleWidgetWidth = (float) totalWidthWithoutMargins / widgets.size(); + + for (int i = 0; i < widgets.size(); i++) { + final AbstractElementWidget widget = widgets.get(i); + final boolean widgetHovered = isMouseOver && (getHoveredWidget(mouseX) == i); + widget.drawScreen(x + (int) ((singleWidgetWidth + 2) * i), y, (int) singleWidgetWidth, slotHeight + 2, mouseX, mouseY, 0, widgetHovered); + + screen.setElementHoveredStatus(widget, widgetHovered); + } + } + + public int getHoveredWidget(int mouseX) { + final float positionAcrossWidget = ((float) MathHelper.clamp_int(mouseX - cachedPosX, 0, cachedWidth)) / cachedWidth; + + return MathHelper.clamp_int((int) Math.floor(widgets.size() * positionAcrossWidget), 0, widgets.size() - 1); + } + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + return this.widgets.get(getHoveredWidget(mouseX)).mouseClicked(mouseX, mouseY, button); + } + + public boolean mouseReleased(int mouseX, int mouseY, int button) { + return this.widgets.get(getHoveredWidget(mouseX)).mouseReleased(mouseX, mouseY, button); + } + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java new file mode 100644 index 000000000..c5ecf1081 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java @@ -0,0 +1,218 @@ +package net.coderbot.iris.gui.element.shaderoptions; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.element.IrisElementRow; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; +import org.jetbrains.annotations.Nullable; + +public class HeaderEntry extends BaseEntry { + + public static final String BACK_BUTTON_TEXT = EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back"); + public static final String RESET_BUTTON_TEXT_INACTIVE = EnumChatFormatting.GRAY + I18n.format("options.iris.reset"); + public static final String RESET_BUTTON_TEXT_ACTIVE = EnumChatFormatting.YELLOW + I18n.format("options.iris.reset"); + + public static final String RESET_HOLD_SHIFT_TOOLTIP = EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift"); + public static final String RESET_TOOLTIP = EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip"); + public static final String IMPORT_TOOLTIP = I18n.format("options.iris.importSettings.tooltip"); + // .withStyle(style -> style.withColor(TextColor.fromRgb(0x4da6ff))); + public static final String EXPORT_TOOLTIP = I18n.format("options.iris.exportSettings.tooltip"); + // .withStyle(style -> style.withColor(TextColor.fromRgb(0xfc7d3d))); + + private static final int MIN_SIDE_BUTTON_WIDTH = 42; + private static final int BUTTON_HEIGHT = 16; + + private final ShaderPackScreen screen; + private final @Nullable IrisElementRow backButton; + private final IrisElementRow utilityButtons = new IrisElementRow(); + private final IrisElementRow.TextButtonElement resetButton; + private final IrisElementRow.IconButtonElement importButton; + private final IrisElementRow.IconButtonElement exportButton; + private final String text; + + public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton) { + super(navigation); + + if (hasBackButton) { + this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(BACK_BUTTON_TEXT, this::backButtonClicked), + Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(BACK_BUTTON_TEXT) + 8)); + } else { + this.backButton = null; + } + + this.resetButton = new IrisElementRow.TextButtonElement(RESET_BUTTON_TEXT_INACTIVE, this::resetButtonClicked); + this.importButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.IMPORT, GuiUtil.Icon.IMPORT_COLORED, this::importSettingsButtonClicked); + this.exportButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.EXPORT, GuiUtil.Icon.EXPORT_COLORED, this::exportSettingsButtonClicked); + + this.utilityButtons.add(this.importButton, 15).add(this.exportButton, 15) + .add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth((RESET_BUTTON_TEXT_INACTIVE) + 8))); + + this.screen = screen; + this.text = text; + } + + public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { + // Draw dividing line + // fill(x - 3, (y + slotHeight) - 2, x + slotWidth, (y + slotHeight) - 1, 0x66BEBEBE); + final int tickDelta = 0; + final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + // Draw header text + this.screen.drawCenteredString(font, text, x + (int) (slotWidth * 0.5), y + 5, 0xFFFFFF); + + GuiUtil.bindIrisWidgetsTexture(); + + // Draw back button if present + if (this.backButton != null) { + backButton.drawScreen(x, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver); + } + + final boolean shiftDown = GuiScreen.isShiftKeyDown(); + + // Set the appearance of the reset button + this.resetButton.disabled = !shiftDown; + this.resetButton.text = shiftDown ? RESET_BUTTON_TEXT_ACTIVE : RESET_BUTTON_TEXT_INACTIVE; + + // Draw the utility buttons + this.utilityButtons.renderRightAligned((x + slotWidth) - 3, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver); + + // Draw the reset button's tooltip + if (this.resetButton.isHovered()) { + final String tooltip = shiftDown ? RESET_TOOLTIP : RESET_HOLD_SHIFT_TOOLTIP; + queueBottomRightAnchoredTooltip(mouseX, mouseY, font, tooltip); + } + // Draw the import/export button tooltips + if (this.importButton.isHovered()) { + queueBottomRightAnchoredTooltip(mouseX, mouseY, font, IMPORT_TOOLTIP); + } + if (this.exportButton.isHovered()) { + queueBottomRightAnchoredTooltip(mouseX, mouseY, font, EXPORT_TOOLTIP); + } + } + + private void queueBottomRightAnchoredTooltip(int x, int y, FontRenderer font, String text) { + ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, text, x - (font.getStringWidth(text) + 10), y - 16)); + } + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + final boolean backButtonResult = backButton != null && backButton.mouseClicked(mouseX, mouseY, button); + final boolean utilButtonResult = utilityButtons.mouseClicked(mouseX, mouseY, button); + + return backButtonResult || utilButtonResult; + } + + private boolean backButtonClicked(IrisElementRow.TextButtonElement button) { + this.navigation.back(); + GuiUtil.playButtonClickSound(); + + return true; + } + + private boolean resetButtonClicked(IrisElementRow.TextButtonElement button) { + if (GuiScreen.isShiftKeyDown()) { + Iris.resetShaderPackOptionsOnNextReload(); + this.screen.applyChanges(); + GuiUtil.playButtonClickSound(); + + return true; + } + + return false; + } + + private boolean importSettingsButtonClicked(IrisElementRow.IconButtonElement button) { + // GuiUtil.playButtonClickSound(); + // + // // Invalid state to be in + // if (!Iris.getCurrentPack().isPresent()) { + // return false; + // } + // + // // Displaying a dialog when the game is full-screened can cause severe issues + // // https://github.com/IrisShaders/Iris/issues/1258 + // if (Minecraft.getMinecraft().isFullScreen()) { + // this.screen.displayNotification( + // "" + EnumChatFormatting.RED + EnumChatFormatting.BOLD + I18n.format("options.iris.mustDisableFullscreen"); + // return false; + // } + // + // final ShaderPackScreen originalScreen = this.screen; // Also used to prevent invalid state + // + // FileDialogUtil.fileSelectDialog( + // FileDialogUtil.DialogType.OPEN, "Import Shader Settings from File", + // Iris.getShaderpacksDirectory().resolve(Iris.getCurrentPackName() + ".txt"), + // "Shader Pack Settings (.txt)", "*.txt") + // .whenComplete((path, err) -> { + // if (err != null) { + // Iris.logger.error("Error selecting shader settings from file", err); + // + // return; + // } + // + // if (Minecraft.getInstance().screen == originalScreen) { + // path.ifPresent(originalScreen::importPackOptions); + // } + // }); + // + return true; + } + + private boolean exportSettingsButtonClicked(IrisElementRow.IconButtonElement button) { + // GuiUtil.playButtonClickSound(); + // + // // Invalid state to be in + // if (!Iris.getCurrentPack().isPresent()) { + // return false; + // } + // + // // Displaying a dialog when the game is full-screened can cause severe issues + // // https://github.com/IrisShaders/Iris/issues/1258 + // if (Minecraft.getInstance().getWindow().isFullscreen()) { + // this.screen.displayNotification( + // I18n.format("options.iris.mustDisableFullscreen") + // .withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD)); + // return false; + // } + // + // FileDialogUtil.fileSelectDialog( + // FileDialogUtil.DialogType.SAVE, "Export Shader Settings to File", + // Iris.getShaderpacksDirectory().resolve(Iris.getCurrentPackName() + ".txt"), + // "Shader Pack Settings (.txt)", "*.txt") + // .whenComplete((path, err) -> { + // if (err != null) { + // Iris.logger.error("Error selecting file to export shader settings", err); + // + // return; + // } + // + // path.ifPresent(p -> { + // Properties toSave = new Properties(); + // + // // Dirty way of getting the currently applied settings as a Properties, directly + // // opens and copies out of the saved settings file if it is present + // Path sourceTxtPath = Iris.getShaderpacksDirectory().resolve(Iris.getCurrentPackName() + ".txt"); + // if (Files.exists(sourceTxtPath)) { + // try (InputStream in = Files.newInputStream(sourceTxtPath)) { + // toSave.load(in); + // } catch (IOException ignored) {} + // } + // + // // Save properties to user determined file + // try (OutputStream out = Files.newOutputStream(p)) { + // toSave.store(out, null); + // } catch (IOException e) { + // Iris.logger.error("Error saving properties to \"" + p + "\"", e); + // } + // }); + // }); + + return true; + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderselection/BaseEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderselection/BaseEntry.java new file mode 100644 index 000000000..5df911356 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderselection/BaseEntry.java @@ -0,0 +1,14 @@ +package net.coderbot.iris.gui.element.shaderselection; + +import net.coderbot.iris.gui.element.IrisGuiSlot; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.renderer.Tessellator; + +public abstract class BaseEntry { + protected IrisGuiSlot list; + protected BaseEntry(IrisGuiSlot list) { + this.list = list; + } + + public abstract void drawEntry(ShaderPackScreen screen, int index, int x, int y, int listWidth, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver); +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderselection/LabelEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderselection/LabelEntry.java new file mode 100644 index 000000000..a94acf858 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderselection/LabelEntry.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.gui.element.shaderselection; + +import net.coderbot.iris.gui.element.IrisGuiSlot; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.renderer.Tessellator; + +public class LabelEntry extends BaseEntry { + private final String label; + + public LabelEntry(IrisGuiSlot list, String label) { + super(list); + this.label = label; + } + + @Override + public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int listWidth, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { + screen.drawCenteredString(label, x, y, 0xFFFFFF); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderselection/ShaderPackEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderselection/ShaderPackEntry.java new file mode 100644 index 000000000..dcb952870 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderselection/ShaderPackEntry.java @@ -0,0 +1,57 @@ +package net.coderbot.iris.gui.element.shaderselection; + +import lombok.Getter; +import net.coderbot.iris.gui.element.ShaderPackSelectionList; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.EnumChatFormatting; + +public class ShaderPackEntry extends BaseEntry { + @Getter private final String packName; + private final ShaderPackSelectionList shaderPackSelectionList; + private final int index; + + public ShaderPackEntry(int index, ShaderPackSelectionList list, String packName) { + super(list); + this.packName = packName; + this.shaderPackSelectionList = list; + this.index = index; + } + + public boolean isApplied() { + return shaderPackSelectionList.getApplied() == this; + } + + public boolean isSelected() { + return shaderPackSelectionList.getSelected() == this; + } + + @Override + public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int listWidth, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { + final FontRenderer font = screen.getFontRenderer(); + final boolean shadersEnabled = shaderPackSelectionList.getTopButtonRow().shadersEnabled; + + int color = 0xFFFFFF; + String name = packName; + if (font.getStringWidth(name) > this.list.getListWidth() - 3) { + name = font.trimStringToWidth(name, this.list.getListWidth() - 8) + "..."; + } + + if(isMouseOver) { + name = EnumChatFormatting.BOLD + name; + } + + if(this.isApplied()) { + color = 0xFFF263; + } + + if(!shadersEnabled && !isMouseOver) { + color = 0xA2A2A2; + } + + + + screen.drawCenteredString(name, (x + listWidth / 2) - 2, y, color); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java new file mode 100644 index 000000000..28470e6a9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java @@ -0,0 +1,89 @@ +package net.coderbot.iris.gui.element.shaderselection; + +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.element.IrisElementRow; +import net.coderbot.iris.gui.element.ShaderPackSelectionList; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.resources.I18n; + +import java.util.function.Function; + +public class TopButtonRowEntry extends BaseEntry { + private static final String NONE_PRESENT_LABEL = I18n.format("options.iris.shaders.nonePresent"); //.withStyle(ChatFormatting.GRAY); + private static final String SHADERS_DISABLED_LABEL = I18n.format("options.iris.shaders.disabled"); + private static final String SHADERS_ENABLED_LABEL = I18n.format("options.iris.shaders.enabled"); + private static final int REFRESH_BUTTON_WIDTH = 18; + + private final ShaderPackSelectionList shaderPackSelectionList; + private final IrisElementRow buttons = new IrisElementRow(); + private final EnableShadersButtonElement enableDisableButton; + private final IrisElementRow.Element refreshPacksButton; + + public boolean allowEnableShadersButton = true; + public boolean shadersEnabled; + + public TopButtonRowEntry(ShaderPackSelectionList list, boolean shadersEnabled) { + super(list); + this.shaderPackSelectionList = list; + this.shadersEnabled = shadersEnabled; + this.enableDisableButton = new EnableShadersButtonElement( + getEnableDisableLabel(), + button -> { + if (this.allowEnableShadersButton) { + setShadersEnabled(!this.shadersEnabled); + GuiUtil.playButtonClickSound(); + return true; + } + + return false; + }); + this.refreshPacksButton = new IrisElementRow.IconButtonElement( + GuiUtil.Icon.REFRESH, + button -> { + this.shaderPackSelectionList.refresh(); + + GuiUtil.playButtonClickSound(); + return true; + }); + this.buttons.add(this.enableDisableButton, 0).add(this.refreshPacksButton, REFRESH_BUTTON_WIDTH); + } + + public void setShadersEnabled(boolean shadersEnabled) { + this.shadersEnabled = shadersEnabled; + this.enableDisableButton.text = getEnableDisableLabel(); + this.shaderPackSelectionList.getScreen().refreshScreenSwitchButton(); + } + + public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int listWidth, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { + this.buttons.setWidth(this.enableDisableButton, (listWidth - 1) - REFRESH_BUTTON_WIDTH); + this.enableDisableButton.centerX = x + (int)(listWidth * 0.5); + + this.buttons.drawScreen(x - 2, y - 3, 18, mouseX, mouseY, 0, isMouseOver); + } + + private String getEnableDisableLabel() { + return this.allowEnableShadersButton ? this.shadersEnabled ? SHADERS_ENABLED_LABEL : SHADERS_DISABLED_LABEL : NONE_PRESENT_LABEL; + } + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return this.buttons.mouseClicked(mouseX, mouseY, button); + } + + // Renders the label at an offset as to not look misaligned with the rest of the menu + public static class EnableShadersButtonElement extends IrisElementRow.TextButtonElement { + private int centerX; + + public EnableShadersButtonElement(String text, Function onClick) { + super(text, onClick); + } + + @Override + public void renderLabel(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + final int textX = this.centerX - (int)(this.font.getStringWidth(this.text) * 0.5); + final int textY = y + (int)((height - 8) * 0.5); + + this.font.drawStringWithShadow(this.text, textX, textY, 0xFFFFFF); + } + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/AbstractElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/AbstractElementWidget.java new file mode 100644 index 000000000..c41c5dab2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/AbstractElementWidget.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement; + +public abstract class AbstractElementWidget { + protected final T element; + + public static final AbstractElementWidget EMPTY = new AbstractElementWidget(null) { + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) {} + }; + + public AbstractElementWidget(T element) { + this.element = element; + } + + public void init(ShaderPackScreen screen, NavigationController navigation) {} + + public abstract void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered); + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + return false; + } + + public boolean mouseReleased(double mx, double my, int button) { + return false; + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java new file mode 100644 index 000000000..eed322d0c --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java @@ -0,0 +1,179 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public abstract class BaseOptionElementWidget extends CommentedElementWidget { + protected static final String SET_TO_DEFAULT = I18n.format("options.iris.setToDefault"); + protected static final String DIVIDER = ": "; + + protected String unmodifiedLabel; + protected ShaderPackScreen screen; + protected NavigationController navigation; + private String label; + + protected String trimmedLabel; + protected String valueLabel; + + private boolean isLabelTrimmed; + private int maxLabelWidth; + private int valueSectionWidth; + + public BaseOptionElementWidget(T element) { + super(element); + } + + @Override + public void init(ShaderPackScreen screen, NavigationController navigation) { + this.screen = screen; + this.navigation = navigation; + this.valueLabel = null; + this.trimmedLabel = null; + } + + protected final void setLabel(String label) { + this.label = label + DIVIDER; + this.unmodifiedLabel = label; + } + + protected final void updateRenderParams(int width, int minValueSectionWidth) { + // Lazy init of value label + if (this.valueLabel == null) { + this.valueLabel = createValueLabel(); + } + + // Determine the width of the value box + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + this.valueSectionWidth = Math.max(minValueSectionWidth, font.getStringWidth(this.valueLabel) + 8); + + // Determine maximum width of trimmed label + this.maxLabelWidth = (width - 8) - this.valueSectionWidth; + + // Lazy init of trimmed label, and make sure it is only trimmed when necessary + if (this.trimmedLabel == null || font.getStringWidth(this.label) > this.maxLabelWidth != isLabelTrimmed) { + updateLabels(); + } + + // Set whether the label has been trimmed (used when updating label and determining whether to render tooltips) + this.isLabelTrimmed = font.getStringWidth(this.label) > this.maxLabelWidth; + } + + protected final void renderOptionWithValue(int x, int y, int width, int height, boolean hovered, float sliderPosition, int sliderWidth) { + GuiUtil.bindIrisWidgetsTexture(); + + // Draw button background + GuiUtil.drawButton(x, y, width, height, hovered, false); + + // Draw the value box + GuiUtil.drawButton((x + width) - (this.valueSectionWidth + 2), y + 2, this.valueSectionWidth, height - 4, false, true); + + // Draw the preview slider + if (sliderPosition >= 0) { + // Range of x values the slider can occupy + int sliderSpace = (this.valueSectionWidth - 4) - sliderWidth; + + // Position of slider + int sliderPos = ((x + width) - this.valueSectionWidth) + (int)(sliderPosition * sliderSpace); + + GuiUtil.drawButton(sliderPos, y + 4, sliderWidth, height - 8, false, false); + } + + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + // Draw the label + font.drawStringWithShadow(this.trimmedLabel, x + 6, y + 7, 0xFFFFFF); + // Draw the value label + font.drawStringWithShadow(this.valueLabel, (x + (width - 2)) - (int)(this.valueSectionWidth * 0.5) - (int)(font.getStringWidth(this.valueLabel) * 0.5), y + 7, 0xFFFFFF); + } + + protected final void renderOptionWithValue(int x, int y, int width, int height, boolean hovered) { + this.renderOptionWithValue(x, y, width, height, hovered, -1, 0); + } + + protected final void tryRenderTooltip(int mouseX, int mouseY, boolean hovered) { + if (GuiScreen.isShiftKeyDown()) { + renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered); + } else if (this.isLabelTrimmed && !this.screen.isDisplayingComment()) { + renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered); + } + } + + protected final void renderTooltip(String text, int mouseX, int mouseY, boolean hovered) { + if (hovered) { + ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(Minecraft.getMinecraft().fontRenderer, text, mouseX + 2, mouseY - 16)); + } + } + + protected final void updateLabels() { + this.trimmedLabel = createTrimmedLabel(); + this.valueLabel = createValueLabel(); + } + + protected final String createTrimmedLabel() { + String label = GuiUtil.shortenText(Minecraft.getMinecraft().fontRenderer, this.label, this.maxLabelWidth); + + if (this.isValueModified()) { + label = label + " (*)"; //.withStyle(style -> style.withColor(TextColor.fromRgb(0xffc94a))); + } + + return label; + } + + protected abstract String createValueLabel(); + + public abstract boolean applyNextValue(); + + public abstract boolean applyPreviousValue(); + + public abstract boolean applyOriginalValue(); + + public abstract boolean isValueModified(); + + public abstract @Nullable String getCommentKey(); + + @Override + public Optional getCommentTitle() { + return Optional.of(this.unmodifiedLabel); + } + + @Override + public Optional getCommentBody() { + return Optional.ofNullable(getCommentKey()).map(I18n::format); + } + + @Override + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0 || button == 1) { + boolean refresh = false; + + if (GuiScreen.isShiftKeyDown()) { + refresh = applyOriginalValue(); + } + if (!refresh) { + if (button == 0) { + refresh = applyNextValue(); + } else { + refresh = applyPreviousValue(); + } + } + + if (refresh) { + this.navigation.refresh(); + } + + GuiUtil.playButtonClickSound(); + + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java new file mode 100644 index 000000000..27bf9e6f6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java @@ -0,0 +1,111 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.BooleanOption; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuBooleanOptionElement; +import net.minecraft.client.resources.I18n; + +public class BooleanElementWidget extends BaseOptionElementWidget { + private static final String TEXT_TRUE = I18n.format("label.iris.true"); + private static final String TEXT_FALSE = I18n.format("label.iris.false"); + private static final String TEXT_TRUE_DEFAULT = I18n.format("label.iris.true"); + private static final String TEXT_FALSE_DEFAULT = I18n.format("label.iris.false"); + + private final BooleanOption option; + + private boolean appliedValue; + private boolean value; + private boolean defaultValue; + + public BooleanElementWidget(OptionMenuBooleanOptionElement element) { + super(element); + + this.option = element.option; + } + + @Override + public void init(ShaderPackScreen screen, NavigationController navigation) { + super.init(screen, navigation); + + // The value currently in use by the shader pack + this.appliedValue = this.element.getAppliedOptionValues().getBooleanValueOrDefault(this.option.getName()); + + // The yet-to-be-applied value that has been queued (if that is the case) + // Might be equal to the applied value + this.value = this.element.getPendingOptionValues().getBooleanValueOrDefault(this.option.getName()); + + this.defaultValue = this.element.getAppliedOptionValues().getOptionSet().getBooleanOptions() + .get(this.option.getName()).getOption().getDefaultValue(); + + this.setLabel(GuiUtil.translateOrDefault(this.option.getName(), "option." + this.option.getName())); + } + + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + this.updateRenderParams(width, 28); + + this.renderOptionWithValue(x, y, width, height, hovered); + this.tryRenderTooltip(mouseX, mouseY, hovered); + } + + @Override + protected String createValueLabel() { + // UX: Do not use color if the value is set to default. + // + // This is because the red color for "Off" and green color of "On" + // was causing people to want to change options to On when that was + // unnecessary due to red having a bad association. + // + // This was changed on request of Emin, since people kept on changing + // Compatibility Mode to "On" when not needed. Now we use white for + // default to avoid giving a positive or negative connotation to a + // default value. + if (this.value == this.defaultValue) { + return this.value ? TEXT_TRUE_DEFAULT : TEXT_FALSE_DEFAULT; + } + + return this.value ? TEXT_TRUE : TEXT_FALSE; + } + + @Override + public String getCommentKey() { + return "option." + this.option.getName() + ".comment"; + } + + public String getValue() { + return Boolean.toString(this.value); + } + + private void queue() { + Iris.getShaderPackOptionQueue().put(this.option.getName(), this.getValue()); + } + + @Override + public boolean applyNextValue() { + this.value = !this.value; + this.queue(); + + return true; + } + + @Override + public boolean applyPreviousValue() { + return this.applyNextValue(); + } + + @Override + public boolean applyOriginalValue() { + this.value = this.option.getDefaultValue(); + this.queue(); + + return true; + } + + @Override + public boolean isValueModified() { + return this.value != this.appliedValue; + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/CommentedElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/CommentedElementWidget.java new file mode 100644 index 000000000..ab623b516 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/CommentedElementWidget.java @@ -0,0 +1,15 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement; + +import java.util.Optional; + +public abstract class CommentedElementWidget extends AbstractElementWidget { + public CommentedElementWidget(T element) { + super(element); + } + + public abstract Optional getCommentTitle(); + + public abstract Optional getCommentBody(); +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/IrisButton.java b/src/main/java/net/coderbot/iris/gui/element/widget/IrisButton.java new file mode 100644 index 000000000..05c65ba7c --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/IrisButton.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.gui.element.widget; + +import net.minecraft.client.gui.GuiButton; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class IrisButton extends GuiButton { + protected final Consumer onPress; + + public IrisButton(int x, int y, int width, int height, String displayString, Consumer onPress) { + super(999, x, y, width, height, displayString); + this.onPress = onPress; + + } + + public void onPress() { + this.onPress.accept(this); + } + + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/IrisImageButton.java b/src/main/java/net/coderbot/iris/gui/element/widget/IrisImageButton.java new file mode 100644 index 000000000..2361e6cdc --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/IrisImageButton.java @@ -0,0 +1,52 @@ +package net.coderbot.iris.gui.element.widget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import cpw.mods.fml.client.config.GuiUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL14; + +import java.util.function.Consumer; + +public class IrisImageButton extends IrisButton { + private final ResourceLocation textureLocation; + private final int xTexStart; + private final int yTexStart; + private final int yDiffTex; + + public IrisImageButton(int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex, ResourceLocation resourceLocation,Consumer onPress) { + super(x, y, width, height, "", onPress); + + this.textureLocation = resourceLocation; + this.xTexStart = xTexStart; + this.yTexStart = yTexStart; + this.yDiffTex = yDiffTex; + } + + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + if(!this.visible) { + return; + } + + int yTex = this.yTexStart; + this.field_146123_n/*isMouseOver*/ = mouseX >= this.xPosition && mouseY >= this.yPosition && mouseX < this.xPosition + this.width && mouseY < this.yPosition + this.height; + if(this.getHoverState(this.field_146123_n) == 2) { + yTex += this.yDiffTex; + } + + // Sets RenderSystem to use solid white as the tint color for blend mode, and enables blend mode + GL14.glBlendColor(1.0f, 1.0f, 1.0f, 1.0f); + GLStateManager.enableBlend(); + + // Sets RenderSystem to be able to use textures when drawing + GLStateManager.enableTexture(); + mc.getTextureManager().bindTexture(this.textureLocation); + + // Draw the texture to the screen + GuiUtils.drawTexturedModalRect(this.xPosition, this.yPosition, this.xTexStart, yTex, width, height, 256); + + } + +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/LinkElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/LinkElementWidget.java new file mode 100644 index 000000000..a3df4da2d --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/LinkElementWidget.java @@ -0,0 +1,85 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuLinkElement; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.resources.I18n; + +import java.util.Optional; + +public class LinkElementWidget extends CommentedElementWidget { + private static final String ARROW = new String(">"); + + private final String targetScreenId; + private final String label; + + private NavigationController navigation; + private String trimmedLabel = null; + private boolean isLabelTrimmed = false; + + public LinkElementWidget(OptionMenuLinkElement element) { + super(element); + + this.targetScreenId = element.targetScreenId; + this.label = GuiUtil.translateOrDefault(new String(element.targetScreenId), "screen." + element.targetScreenId); + } + + @Override + public void init(ShaderPackScreen screen, NavigationController navigation) { + this.navigation = navigation; + } + + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + GuiUtil.bindIrisWidgetsTexture(); + GuiUtil.drawButton(x, y, width, height, hovered, false); + + final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + final int maxLabelWidth = width - 9; + + if (font.getStringWidth(this.label) > maxLabelWidth) { + this.isLabelTrimmed = true; + } + + if (this.trimmedLabel == null) { + this.trimmedLabel = GuiUtil.shortenText(font, this.label, maxLabelWidth); + } + + int labelWidth = font.getStringWidth(this.trimmedLabel); + + font.drawStringWithShadow(this.trimmedLabel, x + (int)(width * 0.5) - (int)(labelWidth * 0.5) - (int)(0.5 * Math.max(labelWidth - (width - 18), 0)), y + 7, 0xFFFFFF); + font.drawString(ARROW, (x + width) - 9, y + 7, 0xFFFFFF); + + if (hovered && this.isLabelTrimmed) { + // To prevent other elements from being drawn on top of the tooltip + ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, this.label, mouseX + 2, mouseY - 16)); + } + } + + @Override + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0) { + this.navigation.open(targetScreenId); + GuiUtil.playButtonClickSound(); + + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public Optional getCommentTitle() { + return Optional.of(this.label); + } + + @Override + public Optional getCommentBody() { + final String translation = "screen." + this.targetScreenId + ".comment"; + final String translated = I18n.format(translation); + return Optional.ofNullable(!translated.equals(translation) ? I18n.format(translation) : null); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/OnPress.java b/src/main/java/net/coderbot/iris/gui/element/widget/OnPress.java new file mode 100644 index 000000000..8feca2ef0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/OnPress.java @@ -0,0 +1,6 @@ +package net.coderbot.iris.gui.element.widget; + +public interface OnPress { + void onPress(IrisButton button); +} + diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/OptionMenuConstructor.java b/src/main/java/net/coderbot/iris/gui/element/widget/OptionMenuConstructor.java new file mode 100644 index 000000000..380c5b593 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/OptionMenuConstructor.java @@ -0,0 +1,87 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.element.ShaderPackOptionList; +import net.coderbot.iris.gui.element.screen.ElementWidgetScreenData; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuBooleanOptionElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuElementScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuLinkElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuMainElementScreen; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuProfileElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuStringOptionElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuSubElementScreen; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public final class OptionMenuConstructor { + private static final Map, WidgetProvider> WIDGET_CREATORS = new HashMap<>(); + private static final Map, ScreenDataProvider> SCREEN_DATA_CREATORS = new HashMap<>(); + + private OptionMenuConstructor() {} + + @SuppressWarnings("unchecked") + public static void registerWidget(Class element, WidgetProvider widget) { + WIDGET_CREATORS.put(element, (WidgetProvider) widget); + } + + @SuppressWarnings("unchecked") + public static void registerScreen(Class screen, ScreenDataProvider data) { + SCREEN_DATA_CREATORS.put(screen, (ScreenDataProvider) data); + } + + public static AbstractElementWidget createWidget(OptionMenuElement element) { + return WIDGET_CREATORS.getOrDefault(element.getClass(), e -> AbstractElementWidget.EMPTY).create(element); + } + + public static ElementWidgetScreenData createScreenData(OptionMenuElementScreen screen) { + return SCREEN_DATA_CREATORS.getOrDefault(screen.getClass(), s -> ElementWidgetScreenData.EMPTY).create(screen); + } + + @SuppressWarnings("unchecked") + public static void constructAndApplyToScreen(OptionMenuContainer container, ShaderPackScreen packScreen, ShaderPackOptionList optionList, NavigationController navigation) { + OptionMenuElementScreen screen = container.mainScreen; + + if (navigation.getCurrentScreen() != null && container.subScreens.containsKey(navigation.getCurrentScreen())) { + screen = container.subScreens.get(navigation.getCurrentScreen()); + } + + ElementWidgetScreenData data = createScreenData(screen); + + optionList.addHeader(data.heading, data.backButton); + optionList.addWidgets(screen.getColumnCount(), screen.elements.stream().map(element -> { + AbstractElementWidget widget = (AbstractElementWidget) createWidget(element); + widget.init(packScreen, navigation); + return widget; + }).collect(Collectors.toList())); + } + + static { + registerScreen(OptionMenuMainElementScreen.class, screen -> + new ElementWidgetScreenData(Iris.getCurrentPackName() + (Iris.isFallback() ? " (fallback)" : ""), false)); + + registerScreen(OptionMenuSubElementScreen.class, screen -> + new ElementWidgetScreenData(GuiUtil.translateOrDefault(new String(screen.screenId), "screen." + screen.screenId), true)); + + registerWidget(OptionMenuBooleanOptionElement.class, BooleanElementWidget::new); + registerWidget(OptionMenuProfileElement.class, ProfileElementWidget::new); + registerWidget(OptionMenuLinkElement.class, LinkElementWidget::new); + + registerWidget(OptionMenuStringOptionElement.class, element -> + element.slider ? new SliderElementWidget(element) : new StringElementWidget(element)); + } + + public interface WidgetProvider { + AbstractElementWidget create(T element); + } + + public interface ScreenDataProvider { + ElementWidgetScreenData create(T screen); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/ProfileElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/ProfileElementWidget.java new file mode 100644 index 000000000..1e1310d6a --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/ProfileElementWidget.java @@ -0,0 +1,100 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.OptionSet; +import net.coderbot.iris.shaderpack.option.Profile; +import net.coderbot.iris.shaderpack.option.ProfileSet; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuProfileElement; +import net.coderbot.iris.shaderpack.option.values.OptionValues; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; + +import java.util.Optional; + +public class ProfileElementWidget extends BaseOptionElementWidget { + private static final String PROFILE_LABEL = I18n.format("options.iris.profile"); + private static final String PROFILE_CUSTOM = I18n.format("options.iris.profile.custom"); + + private Profile next; + private Profile previous; + private String profileLabel; + + public ProfileElementWidget(OptionMenuProfileElement element) { + super(element); + } + + @Override + public void init(ShaderPackScreen screen, NavigationController navigation) { + super.init(screen, navigation); + this.setLabel(PROFILE_LABEL); + + final ProfileSet profiles = this.element.profiles; + final OptionSet options = this.element.options; + final OptionValues pendingValues = this.element.getPendingOptionValues(); + + final ProfileSet.ProfileResult result = profiles.scan(options, pendingValues); + + this.next = result.next; + this.previous = result.previous; + final Optional profileName = result.current.map(p -> p.name); + + this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, "profile." + name)).orElse(PROFILE_CUSTOM); + } + + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(PROFILE_LABEL) + 16)); + + this.renderOptionWithValue(x, y, width, height, hovered); + } + + @Override + protected String createValueLabel() { + return this.profileLabel; + } + + @Override + public Optional getCommentTitle() { + return Optional.of(PROFILE_LABEL); + } + + @Override + public String getCommentKey() { + return "profile.comment"; + } + + @Override + public boolean applyNextValue() { + if (this.next == null) { + return false; + } + + Iris.queueShaderPackOptionsFromProfile(this.next); + + return true; + } + + @Override + public boolean applyPreviousValue() { + if (this.previous == null) { + return false; + } + + Iris.queueShaderPackOptionsFromProfile(this.previous); + + return true; + } + + @Override + public boolean applyOriginalValue() { + return false; // Resetting options is the way to return to the "default profile" + } + + @Override + public boolean isValueModified() { + return false; + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java new file mode 100644 index 000000000..e174d3388 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java @@ -0,0 +1,118 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuStringOptionElement; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.MathHelper; + +public class SliderElementWidget extends StringElementWidget { + private static final int PREVIEW_SLIDER_WIDTH = 4; + private static final int ACTIVE_SLIDER_WIDTH = 6; + + private boolean mouseDown = false; + + public SliderElementWidget(OptionMenuStringOptionElement element) { + super(element); + } + + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + this.updateRenderParams(width, 35); + + if (!hovered) { + this.renderOptionWithValue(x, y, width, height, false, (float)valueIndex / (valueCount - 1), PREVIEW_SLIDER_WIDTH); + } else { + this.renderSlider(x, y, width, height, mouseX, mouseY, tickDelta); + } + + if (GuiScreen.isShiftKeyDown()) { + renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered); + } else if (!this.screen.isDisplayingComment()) { + renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered); + } + + if (this.mouseDown) { + // Release if the mouse went off the slider + if (!hovered) { + this.onReleased(); + } + + whileDragging(x, width, mouseX); + } + } + + private void renderSlider(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta) { + GuiUtil.bindIrisWidgetsTexture(); + + // Draw background button + GuiUtil.drawButton(x, y, width, height, false, false); + // Draw slider area + GuiUtil.drawButton(x + 2, y + 2, width - 4, height - 4, false, true); + + // Range of x values the slider can occupy + final int sliderSpace = (width - 8) - ACTIVE_SLIDER_WIDTH; + // Position of slider + final int sliderPos = (x + 4) + (int)(((float)valueIndex / (valueCount - 1)) * sliderSpace); + // Draw slider + GuiUtil.drawButton(sliderPos, y + 4, ACTIVE_SLIDER_WIDTH, height - 8, this.mouseDown, false); + + // Draw value label + final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + font.drawStringWithShadow(this.valueLabel, (int)(x + (width * 0.5)) - (int)(font.getStringWidth(this.valueLabel) * 0.5), y + 7, 0xFFFFFF); + } + + private void whileDragging(int x, int width, int mouseX) { + final float mousePositionAcrossWidget = MathHelper.clamp_float((float)(mouseX - (x + 4)) / (width - 8), 0, 1); + + final int newValueIndex = Math.min(valueCount - 1, (int)(mousePositionAcrossWidget * valueCount)); + + if (valueIndex != newValueIndex) { + this.valueIndex = newValueIndex; + + this.updateLabels(); + } + } + + private void onReleased() { + mouseDown = false; + + this.queue(); + this.navigation.refresh(); + + GuiUtil.playButtonClickSound(); + } + + @Override + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0) { + if (GuiScreen.isShiftKeyDown()) { + if (this.applyOriginalValue()) { + this.navigation.refresh(); + } + GuiUtil.playButtonClickSound(); + + return true; + } + + mouseDown = true; + GuiUtil.playButtonClickSound(); + + return true; + } + + // Do not use base widget's button click behavior + return false; + } + + @Override + public boolean mouseReleased(double mx, double my, int button) { + if (button == 0) { + this.onReleased(); + + return true; + } + return super.mouseReleased(mx, my, button); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/StringElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/StringElementWidget.java new file mode 100644 index 000000000..a1de6088f --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/element/widget/StringElementWidget.java @@ -0,0 +1,107 @@ +package net.coderbot.iris.gui.element.widget; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.screen.ShaderPackScreen; +import net.coderbot.iris.shaderpack.option.StringOption; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuStringOptionElement; + +import java.util.List; + +public class StringElementWidget extends BaseOptionElementWidget { + protected final StringOption option; + + protected String appliedValue; + protected int valueCount; + protected int valueIndex; + + public StringElementWidget(OptionMenuStringOptionElement element) { + super(element); + + this.option = element.option; + } + + @Override + public void init(ShaderPackScreen screen, NavigationController navigation) { + super.init(screen, navigation); + + // The yet-to-be-applied value that has been queued (if that is the case) + // Might be equal to the applied value + final String actualPendingValue = this.element.getPendingOptionValues().getStringValueOrDefault(this.option.getName()); + + // The value currently in use by the shader pack + this.appliedValue = this.element.getAppliedOptionValues().getStringValueOrDefault(this.option.getName()); + + this.setLabel(GuiUtil.translateOrDefault(this.option.getName(), "option." + this.option.getName())); + + List values = this.option.getAllowedValues(); + + this.valueCount = values.size(); + this.valueIndex = values.indexOf(actualPendingValue); + } + + @Override + public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { + this.updateRenderParams(width, 0); + + this.renderOptionWithValue(x, y, width, height, hovered); + this.tryRenderTooltip(mouseX, mouseY, hovered); + } + + private void increment(int amount) { + this.valueIndex = Math.max(this.valueIndex, 0); + + this.valueIndex = Math.floorMod(this.valueIndex + amount, this.valueCount); + } + + @Override + protected String createValueLabel() { + return GuiUtil.translateOrDefault(getValue(), "value." + this.option.getName() + "." + getValue()); + } + + @Override + public String getCommentKey() { + return "option." + this.option.getName() + ".comment"; + } + + public String getValue() { + if (this.valueIndex < 0) { + return this.appliedValue; + } + return this.option.getAllowedValues().get(this.valueIndex); + } + + protected void queue() { + Iris.getShaderPackOptionQueue().put(this.option.getName(), this.getValue()); + } + + @Override + public boolean applyNextValue() { + this.increment(1); + this.queue(); + + return true; + } + + @Override + public boolean applyPreviousValue() { + this.increment(-1); + this.queue(); + + return true; + } + + @Override + public boolean applyOriginalValue() { + this.valueIndex = this.option.getAllowedValues().indexOf(this.option.getDefaultValue()); + this.queue(); + + return true; + } + + @Override + public boolean isValueModified() { + return !this.appliedValue.equals(this.getValue()); + } +} diff --git a/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java b/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java new file mode 100644 index 000000000..8337106fc --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java @@ -0,0 +1,29 @@ +package net.coderbot.iris.gui.option; + +import java.io.IOException; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; + +public class IrisVideoSettings { + public static int shadowDistance = 32; + + // TODO: Tell the user to check in the shader options once that's supported. + private static final String DISABLED_TOOLTIP = I18n.format("options.iris.shadowDistance.disabled"); + private static final String ENABLED_TOOLTIP = I18n.format("options.iris.shadowDistance.enabled"); + + public static int getOverriddenShadowDistance(int base) { + return Iris.getPipelineManager().getPipeline() + .map(pipeline -> pipeline.getForcedShadowRenderDistanceChunksForDisplay().orElse(base)) + .orElse(base); + } + + public static boolean isShadowDistanceSliderEnabled() { + return Iris.getPipelineManager().getPipeline() + .map(pipeline -> !pipeline.getForcedShadowRenderDistanceChunksForDisplay().isPresent()) + .orElse(true); + } + +} diff --git a/src/main/java/net/coderbot/iris/gui/screen/HudHideable.java b/src/main/java/net/coderbot/iris/gui/screen/HudHideable.java new file mode 100644 index 000000000..71a88e4e2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/screen/HudHideable.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.gui.screen; + +/** + * Screens implementing this will hide the player hand and HUD + * + * Only used for instanceof checks + */ +public interface HudHideable { +} diff --git a/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java b/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java new file mode 100644 index 000000000..c2cd0f7a5 --- /dev/null +++ b/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java @@ -0,0 +1,512 @@ +package net.coderbot.iris.gui.screen; + +import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gui.GuiUtil; +import net.coderbot.iris.gui.NavigationController; +import net.coderbot.iris.gui.element.ShaderPackOptionList; +import net.coderbot.iris.gui.element.ShaderPackSelectionList; +import net.coderbot.iris.gui.element.shaderselection.ShaderPackEntry; +import net.coderbot.iris.gui.element.widget.AbstractElementWidget; +import net.coderbot.iris.gui.element.widget.CommentedElementWidget; +import net.coderbot.iris.gui.element.widget.IrisButton; +import net.coderbot.iris.gui.element.widget.IrisImageButton; +import net.coderbot.iris.shaderpack.ShaderPack; +import net.irisshaders.iris.api.v0.IrisApi; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.Sys; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class ShaderPackScreen extends GuiScreen implements HudHideable { + /** + * Queue rendering to happen on top of all elements. Useful for tooltips or dialogs. + */ + public static final Set TOP_LAYER_RENDER_QUEUE = new HashSet<>(); + + private static final String SELECT_TITLE = I18n.format("pack.iris.select.title"); + private static final String CONFIGURE_TITLE = I18n.format("pack.iris.configure.title"); + private static final int COMMENT_PANEL_WIDTH = 314; + + private final GuiScreen parent; + private final String title; + + private final String irisTextComponent; + + private ShaderPackSelectionList shaderPackList; + + private @Nullable ShaderPackOptionList shaderOptionList = null; + private @Nullable NavigationController navigation = null; + private GuiButton screenSwitchButton; + + private String notificationDialog = null; + private int notificationDialogTimer = 0; + + private @Nullable AbstractElementWidget hoveredElement = null; + private Optional hoveredElementCommentTitle = Optional.empty(); + private List hoveredElementCommentBody = new ArrayList<>(); + private int hoveredElementCommentTimer = 0; + + private boolean optionMenuOpen = false; + + private boolean dropChanges = false; + private static final String development = "Development Environment"; + private String developmentComponent; + private String updateComponent; + + private boolean guiHidden = false; + private boolean dirty = false; + private float guiButtonHoverTimer = 0.0f; + + public ShaderPackScreen(GuiScreen parent) { + this.title = I18n.format("options.iris.shaderPackSelection.title"); + + this.parent = parent; + + String irisName = Iris.MODNAME; // + " " + Iris.getVersion(); // TEMP + + if (Iris.INSTANCE.isDevelopmentEnvironment) { + this.developmentComponent = "Development Environment"; + irisName = irisName.replace("-development-environment", ""); + } + + this.irisTextComponent = irisName; + + refreshForChangedPack(); + } + @Override + public void drawScreen(int mouseX, int mouseY, float delta) { + if(dirty) { + dirty = false; + this.initGui(); + } + + if (this.mc.theWorld == null) { + super.drawDefaultBackground(); + } else if (!this.guiHidden) { + this.drawGradientRect(0, 0, width, height, 0x4F232323, 0x4F232323); + } + + if (!this.guiHidden) { + if (optionMenuOpen && this.shaderOptionList != null) { + this.shaderOptionList.drawScreen(mouseX, mouseY, delta); + } else { + this.shaderPackList.drawScreen(mouseX, mouseY, delta); + } + } + + final float previousHoverTimer = this.guiButtonHoverTimer; + super.drawScreen(mouseX, mouseY, delta); + if (previousHoverTimer == this.guiButtonHoverTimer) { + this.guiButtonHoverTimer = 0.0f; + } + + if (!this.guiHidden) { + drawCenteredString(this.fontRendererObj, this.title, (int) (this.width * 0.5), 8, 0xFFFFFF); + + if (notificationDialog != null && notificationDialogTimer > 0) { + drawCenteredString(this.fontRendererObj, notificationDialog, (int) (this.width * 0.5), 21, 0xFFFFFF); + } else { + if (optionMenuOpen) { + drawCenteredString(this.fontRendererObj, CONFIGURE_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF); + } else { + drawCenteredString(this.fontRendererObj, SELECT_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF); + } + } + + // Draw the comment panel + if (this.isDisplayingComment()) { + // Determine panel height and position + final int panelHeight = Math.max(50, 18 + (this.hoveredElementCommentBody.size() * 10)); + final int x = (int) (0.5 * this.width) - 157; + final int y = this.height - (panelHeight + 4); + // Draw panel + GuiUtil.drawPanel(x, y, COMMENT_PANEL_WIDTH, panelHeight); + // Draw text + this.fontRendererObj.drawStringWithShadow(this.hoveredElementCommentTitle.orElse(""), x + 4, y + 4, 0xFFFFFF); + for (int i = 0; i < this.hoveredElementCommentBody.size(); i++) { + this.fontRendererObj.drawStringWithShadow(this.hoveredElementCommentBody.get(i), x + 4, (y + 16) + (i * 10), 0xFFFFFF); + } + } + } + + // Render everything queued to drawScreen last + for (Runnable render : TOP_LAYER_RENDER_QUEUE) { + render.run(); + } + TOP_LAYER_RENDER_QUEUE.clear(); + + if (this.developmentComponent != null) { + this.fontRendererObj.drawStringWithShadow(developmentComponent, 2, this.height - 10, 0xFFFFFF); + this.fontRendererObj.drawStringWithShadow(irisTextComponent, 2, this.height - 20, 0xFFFFFF); + } else if (this.updateComponent != null) { + this.fontRendererObj.drawStringWithShadow(updateComponent, 2, this.height - 10, 0xFFFFFF); + this.fontRendererObj.drawStringWithShadow(irisTextComponent, 2, this.height - 20, 0xFFFFFF); + } else { + this.fontRendererObj.drawStringWithShadow(irisTextComponent, 2, this.height - 10, 0xFFFFFF); + } + } + + @Override + public void initGui() { + super.initGui(); + final int bottomCenter = this.width / 2 - 50; + final int topCenter = this.width / 2 - 76; + final boolean inWorld = this.mc.theWorld != null; + + this.shaderPackList = new ShaderPackSelectionList(this, this.mc, this.width, this.height, 32, this.height - 58, 0, this.width); + + if (Iris.getCurrentPack().isPresent() && this.navigation != null) { + final ShaderPack currentPack = Iris.getCurrentPack().get(); + + this.shaderOptionList = new ShaderPackOptionList(this, this.navigation, currentPack, this.mc, this.width, this.height, 32, this.height - 58, 0, this.width); + this.navigation.setActiveOptionList(this.shaderOptionList); + + this.shaderOptionList.rebuild(); + } else { + optionMenuOpen = false; + this.shaderOptionList = null; + } + + if (inWorld) { + this.shaderPackList.setRenderBackground(false); + if (shaderOptionList != null) { + this.shaderOptionList.setRenderBackground(false); + } + } + + this.buttonList.clear(); + + if (!this.guiHidden) { + + this.buttonList.add(new IrisButton(bottomCenter + 104, this.height - 27, 100, 20, + I18n.format("gui.done"), button -> this.onClose())); + + this.buttonList.add(new IrisButton(bottomCenter, this.height - 27, 100, 20, + I18n.format("options.iris.apply"), button -> this.applyChanges())); + + this.buttonList.add(new IrisButton(bottomCenter - 104, this.height - 27, 100, 20, + I18n.format("gui.cancel"), button -> this.dropChangesAndClose())); + + this.buttonList.add(new IrisButton(topCenter - 78, this.height - 51, 152, 20, + I18n.format("options.iris.openShaderPackFolder"), button -> this.openShaderPackFolder())); + + this.screenSwitchButton = new IrisButton(topCenter + 78, this.height - 51, 152, 20, + I18n.format("options.iris.shaderPackList"), button -> { + this.optionMenuOpen = !this.optionMenuOpen; + + // UX: Apply changes before switching screens to avoid unintuitive behavior + // + // Not doing this leads to unintuitive behavior, since selecting a pack in the + // list (but not applying) would open the settings for the previous pack, rather + // than opening the settings for the selected (but not applied) pack. + this.applyChanges(); + + initGui(); + } + ); + this.buttonList.add(this.screenSwitchButton); + + refreshScreenSwitchButton(); + } + + if (inWorld) { + final String showOrHide = this.guiHidden + ? I18n.format("options.iris.gui.show") + : I18n.format("options.iris.gui.hide"); + + final float endOfLastButton = this.width / 2.0f + 154.0f; + final float freeSpace = this.width - endOfLastButton; + final int x; + if (freeSpace > 100.0f) { + x = this.width - 50; + } else if (freeSpace < 20.0f) { + x = this.width - 20; + } else { + x = (int) (endOfLastButton + (freeSpace / 2.0f)) - 10; + } + + this.buttonList.add(new IrisImageButton( + x, this.height - 39, + 20, 20, + this.guiHidden ? 20 : 0, 146, 20, + GuiUtil.IRIS_WIDGETS_TEX, + button -> { + this.guiHidden = !this.guiHidden; + this.dirty = true; + } + )); + } + + // NB: Don't let comment remain when exiting options screen + // https://github.com/IrisShaders/Iris/issues/1494 + this.hoveredElement = null; + this.hoveredElementCommentTimer = 0; + } + + + /** + * Called when the mouse is clicked. + */ + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { + boolean handled = false; + if (!this.guiHidden) { + if (optionMenuOpen && this.shaderOptionList != null ) { + handled = this.shaderOptionList.mouseClicked(mouseX, mouseY, mouseButton); + } else { + handled = this.shaderPackList.mouseClicked(mouseX, mouseY, mouseButton); + } + } + if(!handled) { + super.mouseClicked(mouseX, mouseY, mouseButton); + } + } + + /** + * Called when the mouse is moved or a mouse button is released. Signature: (mouseX, mouseY, which) which==-1 is + * mouseMove, which==0 or which==1 is mouseUp + */ + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + boolean handled = false; + if (!this.guiHidden && state != -1) { + if (optionMenuOpen && this.shaderOptionList != null) { + handled = this.shaderOptionList.mouseReleased(mouseX, mouseY, Mouse.getEventButton()); + } else { + handled = this.shaderPackList.mouseReleased(mouseX, mouseY, Mouse.getEventButton()); + } + } + if(!handled) { + super.mouseMovedOrUp(mouseX, mouseY, state); + } + } + + + @Override + protected void actionPerformed(GuiButton guiButton) { + if(guiButton.enabled) { + if(guiButton instanceof IrisButton irisButton) { + irisButton.onPress(); + } + } + } + + public void refreshForChangedPack() { + if (Iris.getCurrentPack().isPresent()) { + final ShaderPack currentPack = Iris.getCurrentPack().get(); + + this.navigation = new NavigationController(currentPack.getMenuContainer()); + + if (this.shaderOptionList != null) { + this.shaderOptionList.applyShaderPack(currentPack); + this.shaderOptionList.rebuild(); + } + } else { + this.navigation = null; + } + + refreshScreenSwitchButton(); + } + + public void refreshScreenSwitchButton() { + if (this.screenSwitchButton != null) { + this.screenSwitchButton.displayString = optionMenuOpen ? I18n.format("options.iris.shaderPackList") : I18n.format("options.iris.shaderPackSettings"); + this.screenSwitchButton.enabled = optionMenuOpen || shaderPackList.getTopButtonRow().shadersEnabled; + } + } + + @Override + protected void keyTyped(char typedChar, int keyCode) { + if (keyCode == Keyboard.KEY_ESCAPE) { + if (this.guiHidden) { + this.guiHidden = false; + this.initGui(); + return; + } else if (this.navigation != null && this.navigation.hasHistory()) { + this.navigation.back(); + return; + } else if (this.optionMenuOpen) { + this.optionMenuOpen = false; + this.initGui(); + return; + } + } + super.keyTyped(typedChar, keyCode); + } + + public void displayNotification(String String) { + this.notificationDialog = String; + this.notificationDialogTimer = 100; + } + + public void onClose() { + if (!dropChanges) { + applyChanges(); + } else { + discardChanges(); + } + + this.mc.displayGuiScreen(parent); + } + + private void dropChangesAndClose() { + dropChanges = true; + onClose(); + } + + public void applyChanges() { + final ShaderPackEntry entry = this.shaderPackList.getSelected(); + + this.shaderPackList.setApplied(entry); + + final String name = entry.getPackName(); + + // If the pack is being changed, clear pending options from the previous pack to + // avoid possible undefined behavior from applying one pack's options to another pack + if (!name.equals(Iris.getCurrentPackName())) { + Iris.clearShaderPackOptionQueue(); + } + + final boolean enabled = this.shaderPackList.getTopButtonRow().shadersEnabled; + + final String previousPackName = Iris.getIrisConfig().getShaderPackName().orElse(null); + final boolean previousShadersEnabled = Iris.getIrisConfig().areShadersEnabled(); + + // Only reload if the pack would be different from before, or shaders were toggled, or options were changed, or if we're about to reset options. + if (!name.equals(previousPackName) || enabled != previousShadersEnabled || !Iris.getShaderPackOptionQueue().isEmpty() || Iris.shouldResetShaderPackOptionsOnNextReload()) { + Iris.getIrisConfig().setShaderPackName(name); + IrisApi.getInstance().getConfig().setShadersEnabledAndApply(enabled); + } + + refreshForChangedPack(); + } + + private void discardChanges() { + Iris.clearShaderPackOptionQueue(); + } + + private void openShaderPackFolder() { + CompletableFuture.runAsync(() -> openUri(Iris.getShaderpacksDirectoryManager().getDirectoryUri())); + } + + private void openUri(URI uri) { + switch (net.minecraft.util.Util.getOSType()) { + case OSX -> { + try { + Runtime.getRuntime().exec(new String[] { "/usr/bin/open", uri.toString() }); + return; + } catch (IOException e) { + e.printStackTrace(); + } + } + case WINDOWS -> { + try { + Runtime.getRuntime().exec(new String[] { "rundll32", "url.dll,FileProtocolHandler", uri.toString() }); + return; + } catch (IOException e) { + e.printStackTrace(); + } + } + case LINUX -> { + try { + Runtime.getRuntime().exec(new String[] { "xdg-open", uri.toString() }); + return; + } catch (IOException e) { + e.printStackTrace(); + } + } + default -> { + } + } + boolean openViaSystemClass = false; + + try { + final Class aClass = Class.forName("java.awt.Desktop"); + final Object getDesktop = aClass.getMethod("getDesktop").invoke((Object) null); + aClass.getMethod("browse", URI.class).invoke(getDesktop, uri); + } catch (Exception e) { + e.printStackTrace(); + openViaSystemClass = true; + } + + if (openViaSystemClass) { + AngelicaTweaker.LOGGER.debug("Opening via system class!"); + Sys.openURL("file://" + uri); + } + } + + // Let the screen know if an element is hovered or not, allowing for accurately updating which element is hovered + public void setElementHoveredStatus(AbstractElementWidget widget, boolean hovered) { + // TODO: Unused, but might be useful for shader options + if (hovered && widget != this.hoveredElement) { + this.hoveredElement = widget; + + if (widget instanceof CommentedElementWidget) { + this.hoveredElementCommentTitle = ((CommentedElementWidget) widget).getCommentTitle(); + + Optional commentBody = ((CommentedElementWidget) widget).getCommentBody(); + if (!commentBody.isPresent()) { + this.hoveredElementCommentBody.clear(); + } else { + String rawCommentBody = commentBody.get(); + + // Strip any trailing "."s + if (rawCommentBody.endsWith(".")) { + rawCommentBody = rawCommentBody.substring(0, rawCommentBody.length() - 1); + } + // Split comment body into lines by separator ". " + List splitByPeriods = Arrays.stream(rawCommentBody.split("\\. [ ]*")).map(String::new).collect(Collectors.toList()); + // Line wrap + this.hoveredElementCommentBody = new ArrayList<>(); + for (String text : splitByPeriods) { + this.hoveredElementCommentBody.addAll(this.fontRendererObj.listFormattedStringToWidth(text, COMMENT_PANEL_WIDTH - 8)); + } + } + } else { + this.hoveredElementCommentTitle = Optional.empty(); + this.hoveredElementCommentBody.clear(); + } + + this.hoveredElementCommentTimer = 0; + } else if (!hovered && widget == this.hoveredElement) { + this.hoveredElement = null; + this.hoveredElementCommentTitle = Optional.empty(); + this.hoveredElementCommentBody.clear(); + this.hoveredElementCommentTimer = 0; + } + } + + public boolean isDisplayingComment() { + return this.hoveredElementCommentTimer > 20 && + this.hoveredElementCommentTitle.isPresent() && + !this.hoveredElementCommentBody.isEmpty(); + } + public void drawCenteredString(String text, int x, int y, int color) { + this.drawCenteredString(this.fontRendererObj, text, x, y, color); + } + public void drawString(String text, int x, int y, int color) { + this.drawString(this.fontRendererObj, text, x, y, color); + } + + + public FontRenderer getFontRenderer() { + return this.fontRendererObj; + } +} diff --git a/src/main/java/net/coderbot/iris/layer/BlockEntityRenderStateShard.java b/src/main/java/net/coderbot/iris/layer/BlockEntityRenderStateShard.java new file mode 100644 index 000000000..67d8ea954 --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/BlockEntityRenderStateShard.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.layer; + +import com.gtnewhorizons.angelica.compat.toremove.RenderPhase; +import net.coderbot.iris.uniforms.CapturedRenderingState; + +public final class BlockEntityRenderStateShard extends RenderPhase { + private static final BlockEntityRenderStateShard UNIDENTIFIED = new BlockEntityRenderStateShard(-1); + + private final int entityId; + + private BlockEntityRenderStateShard(int entityId) { + super("iris:is_block_entity", () -> { + CapturedRenderingState.INSTANCE.setCurrentBlockEntity(entityId); + GbufferPrograms.beginBlockEntities(); + }, () -> { + CapturedRenderingState.INSTANCE.setCurrentBlockEntity(-1); + GbufferPrograms.endBlockEntities(); + }); + + this.entityId = entityId; + } + + public static BlockEntityRenderStateShard forId(int entityId) { + if (entityId == -1) { + return UNIDENTIFIED; + } else { + // TODO: Cache all created render phases to avoid allocations? + return new BlockEntityRenderStateShard(entityId); + } + } + + @Override + public boolean equals(Object object) { + if (object == null || object.getClass() != this.getClass()) { + return false; + } + + BlockEntityRenderStateShard other = (BlockEntityRenderStateShard) object; + + return this.entityId == other.entityId; + } +} diff --git a/src/main/java/net/coderbot/iris/layer/EntityRenderStateShard.java b/src/main/java/net/coderbot/iris/layer/EntityRenderStateShard.java new file mode 100644 index 000000000..d47d627cb --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/EntityRenderStateShard.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.layer; + +import com.gtnewhorizons.angelica.compat.toremove.RenderPhase; +import net.coderbot.iris.uniforms.CapturedRenderingState; + +public final class EntityRenderStateShard extends RenderPhase { + private static final EntityRenderStateShard UNIDENTIFIED = new EntityRenderStateShard(-1); + + private final int entityId; + + private EntityRenderStateShard(int entityId) { + super("iris:is_entity", () -> { + CapturedRenderingState.INSTANCE.setCurrentEntity(entityId); + GbufferPrograms.beginEntities(); + }, () -> { + CapturedRenderingState.INSTANCE.setCurrentEntity(-1); + GbufferPrograms.endEntities(); + }); + + this.entityId = entityId; + } + + public static EntityRenderStateShard forId(int entityId) { + if (entityId == -1) { + return UNIDENTIFIED; + } else { + // TODO: Cache all created render phases to avoid allocations? + return new EntityRenderStateShard(entityId); + } + } + + @Override + public boolean equals(Object object) { + if (object == null || object.getClass() != this.getClass()) { + return false; + } + + EntityRenderStateShard other = (EntityRenderStateShard) object; + + return this.entityId == other.entityId; + } +} diff --git a/src/main/java/net/coderbot/iris/layer/GbufferPrograms.java b/src/main/java/net/coderbot/iris/layer/GbufferPrograms.java new file mode 100644 index 000000000..42f048099 --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/GbufferPrograms.java @@ -0,0 +1,114 @@ +package net.coderbot.iris.layer; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; + +public class GbufferPrograms { + private static boolean entities; + private static boolean blockEntities; + private static boolean outline; + private static Runnable phaseChangeListener; + + private static void checkReentrancy() { + if (entities || blockEntities || outline) { + throw new IllegalStateException("GbufferPrograms in weird state, tried to call begin function when entities = " + + entities + ", blockEntities = " + blockEntities + ", outline = " + outline); + } + } + + public static void beginEntities() { + checkReentrancy(); + setPhase(WorldRenderingPhase.ENTITIES); + entities = true; + } + + public static void endEntities() { + if (!entities) { + throw new IllegalStateException("GbufferPrograms in weird state, tried to call endEntities when entities = false"); + } + + setPhase(WorldRenderingPhase.NONE); + entities = false; + } + + public static void beginOutline() { + checkReentrancy(); + setPhase(WorldRenderingPhase.OUTLINE); + outline = true; + } + + public static void endOutline() { + if (!outline) { + throw new IllegalStateException("GbufferPrograms in weird state, tried to call endOutline when outline = false"); + } + + setPhase(WorldRenderingPhase.NONE); + outline = false; + } + + public static void beginBlockEntities() { + checkReentrancy(); + setPhase(WorldRenderingPhase.BLOCK_ENTITIES); + blockEntities = true; + } + + public static void endBlockEntities() { + if (!blockEntities) { + throw new IllegalStateException("GbufferPrograms in weird state, tried to call endBlockEntities when blockEntities = false"); + } + + setPhase(WorldRenderingPhase.NONE); + blockEntities = false; + } + + public static WorldRenderingPhase getCurrentPhase() { + WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + + if (pipeline != null) { + return pipeline.getPhase(); + } else { + return WorldRenderingPhase.NONE; + } + } + + private static void setPhase(WorldRenderingPhase phase) { + WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + + if (pipeline != null) { + pipeline.setPhase(phase); + } + } + + public static void setOverridePhase(WorldRenderingPhase phase) { + WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + + if (pipeline != null) { + pipeline.setOverridePhase(phase); + } + } + + public static void runPhaseChangeNotifier() { + if (phaseChangeListener != null) { + phaseChangeListener.run(); + } + } + + public static void setupSpecialRenderCondition(SpecialCondition override) { + Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setSpecialCondition(override)); + } + + public static void teardownSpecialRenderCondition(SpecialCondition override) { + Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setSpecialCondition(null)); + } + + static { + StateUpdateNotifiers.phaseChangeNotifier = listener -> phaseChangeListener = listener; + } + + public static void init() { + // Empty initializer to run static + } +} diff --git a/src/main/java/net/coderbot/iris/layer/IsBlockEntityRenderStateShard.java b/src/main/java/net/coderbot/iris/layer/IsBlockEntityRenderStateShard.java new file mode 100644 index 000000000..f18af5669 --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/IsBlockEntityRenderStateShard.java @@ -0,0 +1,11 @@ +package net.coderbot.iris.layer; + +import com.gtnewhorizons.angelica.compat.toremove.RenderPhase; + +public class IsBlockEntityRenderStateShard extends RenderPhase { + public static final IsBlockEntityRenderStateShard INSTANCE = new IsBlockEntityRenderStateShard(); + + private IsBlockEntityRenderStateShard() { + super("iris:is_block_entity", GbufferPrograms::beginBlockEntities, GbufferPrograms::endBlockEntities); + } +} diff --git a/src/main/java/net/coderbot/iris/layer/IsEntityRenderStateShard.java b/src/main/java/net/coderbot/iris/layer/IsEntityRenderStateShard.java new file mode 100644 index 000000000..a3daa0920 --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/IsEntityRenderStateShard.java @@ -0,0 +1,11 @@ +package net.coderbot.iris.layer; + +import com.gtnewhorizons.angelica.compat.toremove.RenderPhase; + +public class IsEntityRenderStateShard extends RenderPhase { + public static final IsEntityRenderStateShard INSTANCE = new IsEntityRenderStateShard(); + + private IsEntityRenderStateShard() { + super("iris:is_entity", GbufferPrograms::beginEntities, GbufferPrograms::endEntities); + } +} diff --git a/src/main/java/net/coderbot/iris/layer/IsOutlineRenderStateShard.java b/src/main/java/net/coderbot/iris/layer/IsOutlineRenderStateShard.java new file mode 100644 index 000000000..2c2edc836 --- /dev/null +++ b/src/main/java/net/coderbot/iris/layer/IsOutlineRenderStateShard.java @@ -0,0 +1,11 @@ +package net.coderbot.iris.layer; + +import com.gtnewhorizons.angelica.compat.toremove.RenderPhase; + +public class IsOutlineRenderStateShard extends RenderPhase { + public static final IsOutlineRenderStateShard INSTANCE = new IsOutlineRenderStateShard(); + + private IsOutlineRenderStateShard() { + super("iris:is_outline", GbufferPrograms::beginOutline, GbufferPrograms::endOutline); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/ClearPass.java b/src/main/java/net/coderbot/iris/pipeline/ClearPass.java new file mode 100644 index 000000000..0d2558715 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/ClearPass.java @@ -0,0 +1,46 @@ +package net.coderbot.iris.pipeline; + +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.minecraft.client.Minecraft; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL11; + +import java.util.Objects; +import java.util.function.IntSupplier; + +public class ClearPass { + private final Vector4f color; + private final IntSupplier viewportX; + private final IntSupplier viewportY; + private final GlFramebuffer framebuffer; + private final int clearFlags; + + public ClearPass(Vector4f color, IntSupplier viewportX, IntSupplier viewportY, GlFramebuffer framebuffer, int clearFlags) { + this.color = color; + this.viewportX = viewportX; + this.viewportY = viewportY; + this.framebuffer = framebuffer; + this.clearFlags = clearFlags; + } + + public void execute(Vector4f defaultClearColor) { + GL11.glViewport(0, 0, viewportX.getAsInt(), viewportY.getAsInt()); + framebuffer.bind(); + + Vector4f color = Objects.requireNonNull(defaultClearColor); + + if (this.color != null) { + color = this.color; + } + + GL11.glClearColor(color.x, color.y, color.z, color.w); + GL11.glClear(clearFlags); + if (Minecraft.isRunningOnMac) { + GL11.glGetError(); + } + } + + public GlFramebuffer getFramebuffer() { + return framebuffer; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/ClearPassCreator.java b/src/main/java/net/coderbot/iris/pipeline/ClearPassCreator.java new file mode 100644 index 000000000..a8c175703 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/ClearPassCreator.java @@ -0,0 +1,127 @@ +package net.coderbot.iris.pipeline; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import net.coderbot.iris.shaderpack.PackShadowDirectives; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import org.joml.Vector2i; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ClearPassCreator { + public static ImmutableList createClearPasses(RenderTargets renderTargets, boolean fullClear, + PackRenderTargetDirectives renderTargetDirectives) { + final int maxDrawBuffers = GL11.glGetInteger(GL20.GL_MAX_DRAW_BUFFERS); + + // Sort buffers by their clear color so we can group up glClear calls. + Map> clearByColor = new HashMap<>(); + + renderTargetDirectives.getRenderTargetSettings().forEach((bufferI, settings) -> { + // unboxed + final int buffer = bufferI; + + if (fullClear || settings.shouldClear()) { + Vector4f defaultClearColor; + + if (buffer == 0) { + // colortex0 is cleared to the fog color (with 1.0 alpha) by default. + defaultClearColor = null; + } else if (buffer == 1) { + // colortex1 is cleared to solid white (with 1.0 alpha) by default. + defaultClearColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + } else { + // all other buffers are cleared to solid black (with 0.0 alpha) by default. + defaultClearColor = new Vector4f(0.0f, 0.0f, 0.0f, 0.0f); + } + + RenderTarget target = renderTargets.get(buffer); + Vector4f clearColor = settings.getClearColor().orElse(defaultClearColor); + clearByColor.computeIfAbsent(new Vector2i(target.getWidth(), target.getHeight()), size -> new HashMap<>()).computeIfAbsent(new ClearPassInformation(clearColor, target.getWidth(), target.getHeight()), color -> new IntArrayList()).add(buffer); + } + }); + + List clearPasses = new ArrayList<>(); + + clearByColor.forEach((passSize, vector4fIntListMap) -> { + vector4fIntListMap.forEach((clearInfo, buffers) -> { + int startIndex = 0; + + while (startIndex < buffers.size()) { + // clear up to the maximum number of draw buffers per each clear pass. + // This allows us to handle having more than 8 buffers with the same clear color on systems with + // a max draw buffers of 8 (ie, most systems). + int[] clearBuffers = new int[Math.min(buffers.size() - startIndex, maxDrawBuffers)]; + + for (int i = 0; i < clearBuffers.length; i++) { + clearBuffers[i] = buffers.getInt(startIndex); + startIndex++; + } + + // No need to clear the depth buffer, since we're using Minecraft's depth buffer. + clearPasses.add(new ClearPass(clearInfo.getColor(), clearInfo::getWidth, clearInfo::getHeight, + renderTargets.createClearFramebuffer(true, clearBuffers), GL11.GL_COLOR_BUFFER_BIT)); + clearPasses.add(new ClearPass(clearInfo.getColor(), clearInfo::getWidth, clearInfo::getHeight, + renderTargets.createClearFramebuffer(false, clearBuffers), GL11.GL_COLOR_BUFFER_BIT)); + } + }); + }); + + return ImmutableList.copyOf(clearPasses); + } + + public static ImmutableList createShadowClearPasses(ShadowRenderTargets renderTargets, boolean fullClear, + PackShadowDirectives renderTargetDirectives) { + final int maxDrawBuffers = GL11.glGetInteger(GL20.GL_MAX_DRAW_BUFFERS); + + // Sort buffers by their clear color so we can group up glClear calls. + Map clearByColor = new HashMap<>(); + + for (int i = 0; i < renderTargetDirectives.getColorSamplingSettings().size(); i++) { + // unboxed + PackShadowDirectives.SamplingSettings settings = renderTargetDirectives.getColorSamplingSettings().get(i); + + if (fullClear || settings.getClear()) { + Vector4f clearColor = settings.getClearColor(); + clearByColor.computeIfAbsent(clearColor, color -> new IntArrayList()).add(i); + } + } + + List clearPasses = new ArrayList<>(); + + + clearByColor.forEach((clearColor, buffers) -> { + int startIndex = 0; + + while (startIndex < buffers.size()) { + // clear up to the maximum number of draw buffers per each clear pass. + // This allows us to handle having more than 8 buffers with the same clear color on systems with + // a max draw buffers of 8 (ie, most systems). + int[] clearBuffers = new int[Math.min(buffers.size() - startIndex, maxDrawBuffers)]; + + for (int i = 0; i < clearBuffers.length; i++) { + clearBuffers[i] = buffers.getInt(startIndex); + startIndex++; + } + + // No need to clear the depth buffer, since we're using Minecraft's depth buffer. + clearPasses.add(new ClearPass(clearColor, renderTargets::getResolution, renderTargets::getResolution, + renderTargets.createFramebufferWritingToAlt(clearBuffers), GL11.GL_COLOR_BUFFER_BIT)); + + clearPasses.add(new ClearPass(clearColor, renderTargets::getResolution, renderTargets::getResolution, + renderTargets.createFramebufferWritingToMain(clearBuffers), GL11.GL_COLOR_BUFFER_BIT)); + } + }); + + return ImmutableList.copyOf(clearPasses); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/ClearPassInformation.java b/src/main/java/net/coderbot/iris/pipeline/ClearPassInformation.java new file mode 100644 index 000000000..42197a5dc --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/ClearPassInformation.java @@ -0,0 +1,38 @@ +package net.coderbot.iris.pipeline; + +import org.joml.Vector4f; + +public class ClearPassInformation { + private final Vector4f color; + private final int width; + private final int height; + + public ClearPassInformation(Vector4f vector4f, int width, int height) { + this.color = vector4f; + this.width = width; + this.height = height; + } + + public Vector4f getColor() { + return color; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ClearPassInformation)) { + return false; + } + + ClearPassInformation information = (ClearPassInformation) obj; + + return information.color.equals(this.color) && information.height == this.height && information.width == this.width; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/CustomTextureManager.java b/src/main/java/net/coderbot/iris/pipeline/CustomTextureManager.java new file mode 100644 index 000000000..a56b7d716 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/CustomTextureManager.java @@ -0,0 +1,176 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.rendertarget.NativeImageBackedCustomTexture; +import net.coderbot.iris.rendertarget.NativeImageBackedNoiseTexture; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shaderpack.texture.CustomTextureData; +import net.coderbot.iris.shaderpack.texture.TextureStage; +import net.coderbot.iris.texture.format.TextureFormat; +import net.coderbot.iris.texture.format.TextureFormatLoader; +import net.coderbot.iris.texture.pbr.PBRTextureHolder; +import net.coderbot.iris.texture.pbr.PBRTextureManager; +import net.coderbot.iris.texture.pbr.PBRType; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.ITextureObject; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.renderer.texture.TextureUtil; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.opengl.GL11; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Optional; +import java.util.function.IntSupplier; + +public class CustomTextureManager { + private final EnumMap> customTextureIdMap = new EnumMap<>(TextureStage.class); + private final IntSupplier noise; + + /** + * List of all OpenGL texture objects owned by this CustomTextureManager that need to be deleted in order to avoid + * leaks. + * Make sure any textures added to this list call releaseId from the close method. + */ + private final List ownedTextures = new ArrayList<>(); + + public CustomTextureManager(PackDirectives packDirectives, + EnumMap> customTextureDataMap, + Optional customNoiseTextureData) { + customTextureDataMap.forEach((textureStage, customTextureStageDataMap) -> { + Object2ObjectMap customTextureIds = new Object2ObjectOpenHashMap<>(); + + customTextureStageDataMap.forEach((samplerName, textureData) -> { + try { + customTextureIds.put(samplerName, createCustomTexture(textureData)); + } catch (IOException e) { + Iris.logger.error("Unable to parse the image data for the custom texture on stage " + + textureStage + ", sampler " + samplerName, e); + } + }); + + customTextureIdMap.put(textureStage, customTextureIds); + }); + + noise = customNoiseTextureData.flatMap(textureData -> { + try { + return Optional.of(createCustomTexture(textureData)); + } catch (IOException e) { + Iris.logger.error("Unable to parse the image data for the custom noise texture", e); + + return Optional.empty(); + } + }).orElseGet(() -> { + final int noiseTextureResolution = packDirectives.getNoiseTextureResolution(); + + AbstractTexture texture = new NativeImageBackedNoiseTexture(noiseTextureResolution); + ownedTextures.add(texture); + + return texture::getGlTextureId; + }); + } + + private IntSupplier createCustomTexture(CustomTextureData textureData) throws IOException { + if (textureData instanceof CustomTextureData.PngData) { + AbstractTexture texture = new NativeImageBackedCustomTexture((CustomTextureData.PngData) textureData); + ownedTextures.add(texture); + + return texture::getGlTextureId; + } + // TODO: Iris +// else if (textureData instanceof CustomTextureData.LightmapMarker) { +// // Special code path for the light texture. While shader packs hardcode the primary light texture, it's +// // possible that a mod will create a different light texture, so this code path is robust to that. +// return () -> ((LightTextureAccessor) Minecraft.getMinecraft().gameRenderer.lightTexture()).getLightTexture().getId(); +// } + else if (textureData instanceof CustomTextureData.ResourceData) { + CustomTextureData.ResourceData resourceData = (CustomTextureData.ResourceData) textureData; + String namespace = resourceData.getNamespace(); + String location = resourceData.getLocation(); + + String withoutExtension; + int extensionIndex = FilenameUtils.indexOfExtension(location); + if (extensionIndex != -1) { + withoutExtension = location.substring(0, extensionIndex); + } else { + withoutExtension = location; + } + PBRType pbrType = PBRType.fromFileLocation(withoutExtension); + + TextureManager textureManager = Minecraft.getMinecraft().getTextureManager(); + + if (pbrType == null) { + ResourceLocation textureLocation = new ResourceLocation(namespace, location); + + // NB: We have to re-query the TextureManager for the texture object every time. This is because the + // AbstractTexture object could be removed / deleted from the TextureManager on resource reloads, + // and we could end up holding on to a deleted texture unless we added special code to handle resource + // reloads. Re-fetching the texture from the TextureManager every time is the most robust approach for + // now. + return () -> { + ITextureObject texture = textureManager.getTexture(textureLocation); + + // TODO: Should we give something else if the texture isn't there? This will need some thought + return texture != null ? texture.getGlTextureId() : TextureUtil.missingTexture.getGlTextureId(); + }; + } else { + location = location.substring(0, extensionIndex - pbrType.getSuffix().length()) + location.substring(extensionIndex); + ResourceLocation textureLocation = new ResourceLocation(namespace, location); + + return () -> { + ITextureObject texture = textureManager.getTexture(textureLocation); + + if (texture != null) { + int id = texture.getGlTextureId(); + PBRTextureHolder pbrHolder = PBRTextureManager.INSTANCE.getOrLoadHolder(id); + AbstractTexture pbrTexture = switch (pbrType) { + case NORMAL -> pbrHolder.getNormalTexture(); + case SPECULAR -> pbrHolder.getSpecularTexture(); + default -> throw new Error("Unknown PBRType '" + pbrType + "'"); + }; + + TextureFormat textureFormat = TextureFormatLoader.getFormat(); + if (textureFormat != null) { + int previousBinding = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, pbrTexture.getGlTextureId()); + textureFormat.setupTextureParameters(pbrType, pbrTexture); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, previousBinding); + } + + return pbrTexture.getGlTextureId(); + } + +// return MissingTextureAtlasSprite.getTexture().getId(); + return TextureUtil.missingTexture.getGlTextureId(); + }; + } + } else { + throw new IllegalArgumentException("Unable to handle custom texture data " + textureData); + } + } + + public EnumMap> getCustomTextureIdMap() { + return customTextureIdMap; + } + + public Object2ObjectMap getCustomTextureIdMap(TextureStage stage) { + return customTextureIdMap.getOrDefault(stage, Object2ObjectMaps.emptyMap()); + } + + public IntSupplier getNoiseTexture() { + return noise; + } + + public void destroy() { + // TODO :Iris +// ownedTextures.forEach(AbstractTexture::close); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java new file mode 100644 index 000000000..67b0c104c --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java @@ -0,0 +1,1247 @@ +package net.coderbot.iris.pipeline; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Ints; +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.Iris; +import net.coderbot.iris.block_rendering.BlockMaterialMapping; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.gbuffer_overrides.matching.ProgramTable; +import net.coderbot.iris.gbuffer_overrides.matching.RenderCondition; +import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; +import net.coderbot.iris.gbuffer_overrides.state.RenderTargetStateListener; +import net.coderbot.iris.gl.blending.AlphaTestOverride; +import net.coderbot.iris.gl.blending.BlendModeOverride; +import net.coderbot.iris.gl.blending.BufferBlendOverride; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.program.ComputeProgram; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.program.ProgramImages; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.texture.DepthBufferFormat; +import net.coderbot.iris.layer.GbufferPrograms; +import net.coderbot.iris.pipeline.transform.PatchShaderType; +import net.coderbot.iris.pipeline.transform.TransformPatcher; +import net.coderbot.iris.postprocess.BufferFlipper; +import net.coderbot.iris.postprocess.CenterDepthSampler; +import net.coderbot.iris.postprocess.CompositeRenderer; +import net.coderbot.iris.postprocess.FinalPassRenderer; +import net.coderbot.iris.rendertarget.IRenderTargetExt; +import net.coderbot.iris.rendertarget.NativeImageBackedSingleColorTexture; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.samplers.IrisImages; +import net.coderbot.iris.samplers.IrisSamplers; +import net.coderbot.iris.shaderpack.CloudSetting; +import net.coderbot.iris.shaderpack.ComputeSource; +import net.coderbot.iris.shaderpack.IdMap; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shaderpack.PackShadowDirectives; +import net.coderbot.iris.shaderpack.ProgramDirectives; +import net.coderbot.iris.shaderpack.ProgramFallbackResolver; +import net.coderbot.iris.shaderpack.ProgramSet; +import net.coderbot.iris.shaderpack.ProgramSource; +import net.coderbot.iris.shaderpack.loading.ProgramId; +import net.coderbot.iris.shaderpack.texture.TextureStage; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import net.coderbot.iris.texture.TextureInfoCache; +import net.coderbot.iris.texture.format.TextureFormat; +import net.coderbot.iris.texture.format.TextureFormatLoader; +import net.coderbot.iris.texture.pbr.PBRTextureHolder; +import net.coderbot.iris.texture.pbr.PBRTextureManager; +import net.coderbot.iris.texture.pbr.PBRType; +import net.coderbot.iris.uniforms.CommonUniforms; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.shader.Framebuffer; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL30; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.Set; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +/** + * Encapsulates the compiled shader program objects for the currently loaded shaderpack. + */ +public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, RenderTargetStateListener { + private final static int SRC_ALPHA = 770; + private final static int ONE_MINUS_SRC_ALPHA = 771; + private final static int ONE = 1; + private final RenderTargets renderTargets; + + @Nullable + private ShadowRenderTargets shadowRenderTargets; + @Nullable + private ComputeProgram[] shadowComputes; + private final Supplier shadowTargetsSupplier; + + private final ProgramTable table; + + private ImmutableList clearPassesFull; + private ImmutableList clearPasses; + private ImmutableList shadowClearPasses; + private ImmutableList shadowClearPassesFull; + + private final CompositeRenderer prepareRenderer; + + @Nullable + private final ShadowRenderer shadowRenderer; + + private final int shadowMapResolution; + private final CompositeRenderer deferredRenderer; + private final CompositeRenderer compositeRenderer; + private final FinalPassRenderer finalPassRenderer; + private final CustomTextureManager customTextureManager; + private final AbstractTexture whitePixel; + private final FrameUpdateNotifier updateNotifier; + private final CenterDepthSampler centerDepthSampler; + + private final ImmutableSet flippedBeforeShadow; + private final ImmutableSet flippedAfterPrepare; + private final ImmutableSet flippedAfterTranslucent; + + private final SodiumTerrainPipeline sodiumTerrainPipeline; + + private final HorizonRenderer horizonRenderer = new HorizonRenderer(); + + private final float sunPathRotation; + private final CloudSetting cloudSetting; + private final boolean shouldRenderUnderwaterOverlay; + private final boolean shouldRenderVignette; + private final boolean shouldRenderSun; + private final boolean shouldRenderMoon; + private final boolean shouldWriteRainAndSnowToDepthBuffer; + private final boolean shouldRenderParticlesBeforeDeferred; + private final boolean shouldRenderPrepareBeforeShadow; + private final boolean oldLighting; + private final boolean allowConcurrentCompute; + private final OptionalInt forcedShadowRenderDistanceChunks; + + private Pass current = null; + + private WorldRenderingPhase overridePhase = null; + private WorldRenderingPhase phase = WorldRenderingPhase.NONE; + private boolean isBeforeTranslucent; + private boolean isRenderingShadow = false; + private InputAvailability inputs = new InputAvailability(false, false, false); + private SpecialCondition special = null; + + private boolean shouldBindPBR; + private int currentNormalTexture; + private int currentSpecularTexture; + private PackDirectives packDirectives; + + public DeferredWorldRenderingPipeline(ProgramSet programs) { + Objects.requireNonNull(programs); + + this.cloudSetting = programs.getPackDirectives().getCloudSetting(); + this.shouldRenderUnderwaterOverlay = programs.getPackDirectives().underwaterOverlay(); + this.shouldRenderVignette = programs.getPackDirectives().vignette(); + this.shouldRenderSun = programs.getPackDirectives().shouldRenderSun(); + this.shouldRenderMoon = programs.getPackDirectives().shouldRenderMoon(); + this.shouldWriteRainAndSnowToDepthBuffer = programs.getPackDirectives().rainDepth(); + this.shouldRenderParticlesBeforeDeferred = programs.getPackDirectives().areParticlesBeforeDeferred(); + this.allowConcurrentCompute = programs.getPackDirectives().getConcurrentCompute(); + this.shouldRenderPrepareBeforeShadow = programs.getPackDirectives().isPrepareBeforeShadow(); + this.oldLighting = programs.getPackDirectives().isOldLighting(); + this.updateNotifier = new FrameUpdateNotifier(); + + this.packDirectives = programs.getPackDirectives(); + + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + + final int depthTextureId = ((IRenderTargetExt)main).getIris$depthTextureId(); + final int internalFormat = TextureInfoCache.INSTANCE.getInfo(depthTextureId).getInternalFormat(); + final DepthBufferFormat depthBufferFormat = DepthBufferFormat.fromGlEnumOrDefault(internalFormat); + + this.renderTargets = new RenderTargets(main.framebufferWidth, main.framebufferHeight, depthTextureId, + ((IRenderTargetExt)main).iris$getDepthBufferVersion(), depthBufferFormat, + programs.getPackDirectives().getRenderTargetDirectives().getRenderTargetSettings(), programs.getPackDirectives()); + + this.sunPathRotation = programs.getPackDirectives().getSunPathRotation(); + + PackShadowDirectives shadowDirectives = programs.getPackDirectives().getShadowDirectives(); + + if (shadowDirectives.isDistanceRenderMulExplicit()) { + if (shadowDirectives.getDistanceRenderMul() >= 0.0) { + // add 15 and then divide by 16 to ensure we're rounding up + forcedShadowRenderDistanceChunks = OptionalInt.of(((int) (shadowDirectives.getDistance() * shadowDirectives.getDistanceRenderMul()) + 15) / 16); + } else { + forcedShadowRenderDistanceChunks = OptionalInt.of(-1); + } + } else { + forcedShadowRenderDistanceChunks = OptionalInt.empty(); + } + + // TODO: BlockStateIdMap + BlockRenderingSettings.INSTANCE.setBlockMatches(BlockMaterialMapping.createBlockStateIdMap(programs.getPack().getIdMap().getBlockProperties())); + BlockRenderingSettings.INSTANCE.setBlockTypeIds(BlockMaterialMapping.createBlockTypeMap(programs.getPack().getIdMap().getBlockRenderTypeMap())); + + BlockRenderingSettings.INSTANCE.setEntityIds(programs.getPack().getIdMap().getEntityIdMap()); + BlockRenderingSettings.INSTANCE.setAmbientOcclusionLevel(programs.getPackDirectives().getAmbientOcclusionLevel()); + BlockRenderingSettings.INSTANCE.setDisableDirectionalShading(shouldDisableDirectionalShading()); + BlockRenderingSettings.INSTANCE.setUseSeparateAo(programs.getPackDirectives().shouldUseSeparateAo()); + BlockRenderingSettings.INSTANCE.setUseExtendedVertexFormat(true); + + // Don't clobber anything in texture unit 0. It probably won't cause issues, but we're just being cautious here. + GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); + + customTextureManager = new CustomTextureManager(programs.getPackDirectives(), programs.getPack().getCustomTextureDataMap(), programs.getPack().getCustomNoiseTexture()); + + whitePixel = new NativeImageBackedSingleColorTexture(255, 255, 255, 255); + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + this.flippedBeforeShadow = ImmutableSet.of(); + + BufferFlipper flipper = new BufferFlipper(); + + this.centerDepthSampler = new CenterDepthSampler(() -> getRenderTargets().getDepthTexture(), programs.getPackDirectives().getCenterDepthHalfLife()); + + this.shadowMapResolution = programs.getPackDirectives().getShadowDirectives().getResolution(); + + this.shadowTargetsSupplier = () -> { + if (shadowRenderTargets == null) { + this.shadowRenderTargets = new ShadowRenderTargets(shadowMapResolution, shadowDirectives); + } + + return shadowRenderTargets; + }; + + PatchedShaderPrinter.resetPrintState(); + + this.prepareRenderer = new CompositeRenderer(programs.getPackDirectives(), programs.getPrepare(), programs.getPrepareCompute(), renderTargets, + customTextureManager.getNoiseTexture(), updateNotifier, centerDepthSampler, flipper, shadowTargetsSupplier, + customTextureManager.getCustomTextureIdMap(TextureStage.PREPARE), + programs.getPackDirectives().getExplicitFlips("prepare_pre")); + + flippedAfterPrepare = flipper.snapshot(); + + this.deferredRenderer = new CompositeRenderer(programs.getPackDirectives(), programs.getDeferred(), programs.getDeferredCompute(), renderTargets, + customTextureManager.getNoiseTexture(), updateNotifier, centerDepthSampler, flipper, shadowTargetsSupplier, + customTextureManager.getCustomTextureIdMap(TextureStage.DEFERRED), + programs.getPackDirectives().getExplicitFlips("deferred_pre")); + + flippedAfterTranslucent = flipper.snapshot(); + + this.compositeRenderer = new CompositeRenderer(programs.getPackDirectives(), programs.getComposite(), programs.getCompositeCompute(), renderTargets, + customTextureManager.getNoiseTexture(), updateNotifier, centerDepthSampler, flipper, shadowTargetsSupplier, + customTextureManager.getCustomTextureIdMap(TextureStage.COMPOSITE_AND_FINAL), + programs.getPackDirectives().getExplicitFlips("composite_pre")); + this.finalPassRenderer = new FinalPassRenderer(programs, renderTargets, customTextureManager.getNoiseTexture(), updateNotifier, flipper.snapshot(), + centerDepthSampler, shadowTargetsSupplier, + customTextureManager.getCustomTextureIdMap(TextureStage.COMPOSITE_AND_FINAL), + this.compositeRenderer.getFlippedAtLeastOnceFinal()); + + // [(textured=false,lightmap=false), (textured=true,lightmap=false), (textured=true,lightmap=true)] + ProgramId[] ids = new ProgramId[] { + ProgramId.Basic, ProgramId.Textured, ProgramId.TexturedLit, + ProgramId.SkyBasic, ProgramId.SkyTextured, ProgramId.SkyTextured, + null, null, ProgramId.Terrain, + null, null, ProgramId.Water, + null, ProgramId.Clouds, ProgramId.Clouds, + null, ProgramId.DamagedBlock, ProgramId.DamagedBlock, + ProgramId.Block, ProgramId.Block, ProgramId.Block, + ProgramId.BeaconBeam, ProgramId.BeaconBeam, ProgramId.BeaconBeam, + ProgramId.Entities, ProgramId.Entities, ProgramId.Entities, + ProgramId.EntitiesTrans, ProgramId.EntitiesTrans, ProgramId.EntitiesTrans, + null, ProgramId.ArmorGlint, ProgramId.ArmorGlint, + null, ProgramId.SpiderEyes, ProgramId.SpiderEyes, + ProgramId.Hand, ProgramId.Hand, ProgramId.Hand, + ProgramId.HandWater, ProgramId.HandWater, ProgramId.HandWater, + null, null, ProgramId.Weather, + // world border uses textured_lit even though it has no lightmap :/ + null, ProgramId.TexturedLit, ProgramId.TexturedLit, + ProgramId.Shadow, ProgramId.Shadow, ProgramId.Shadow + }; + + if (ids.length != RenderCondition.values().length * 3) { + throw new IllegalStateException("Program ID table length mismatch"); + } + + ProgramFallbackResolver resolver = new ProgramFallbackResolver(programs); + + Map, Pass> cachedPasses = new HashMap<>(); + + this.shadowComputes = createShadowComputes(programs.getShadowCompute(), programs); + + this.table = new ProgramTable<>((condition, availability) -> { + int idx; + + if (availability.texture && availability.lightmap) { + idx = 2; + } else if (availability.texture) { + idx = 1; + } else { + idx = 0; + } + + ProgramId id = ids[condition.ordinal() * 3 + idx]; + + if (id == null) { + id = ids[idx]; + } + + ProgramId finalId = id; + + return cachedPasses.computeIfAbsent(Pair.of(id, availability), p -> { + ProgramSource source = resolver.resolveNullable(p.getLeft()); + + if (condition == RenderCondition.SHADOW) { + if (!shadowDirectives.isShadowEnabled().orElse(shadowRenderTargets != null)) { + // shadow is not used + return null; + } else if (source == null) { + // still need the custom framebuffer, viewport, and blend mode behavior + GlFramebuffer shadowFb = shadowTargetsSupplier.get().createShadowFramebuffer(shadowRenderTargets.snapshot(), new int[] {0}); + return new Pass(null, shadowFb, shadowFb, null, + BlendModeOverride.OFF, Collections.emptyList(), true); + } + } + + if (source == null) { + return createDefaultPass(); + } + + try { + return createPass(source, availability, condition == RenderCondition.SHADOW, finalId); + } catch (Exception e) { + throw new RuntimeException("Failed to create pass for " + source.getName() + " for rendering condition " + + condition + " specialized to input availability " + availability, e); + } + }); + }); + if (shadowRenderTargets == null && shadowDirectives.isShadowEnabled() == OptionalBoolean.TRUE) { + shadowRenderTargets = new ShadowRenderTargets(shadowMapResolution, shadowDirectives); + } + + if (shadowRenderTargets != null) { + this.shadowClearPasses = ClearPassCreator.createShadowClearPasses(shadowRenderTargets, false, shadowDirectives); + this.shadowClearPassesFull = ClearPassCreator.createShadowClearPasses(shadowRenderTargets, true, shadowDirectives); + + if (programs.getPackDirectives().getShadowDirectives().isShadowEnabled().orElse(true)) { + this.shadowRenderer = new ShadowRenderer(programs.getShadow().orElse(null), + programs.getPackDirectives(), shadowRenderTargets); + Program shadowProgram = table.match(RenderCondition.SHADOW, new InputAvailability(true, true, true)).getProgram(); + shadowRenderer.setUsesImages(shadowProgram != null && shadowProgram.getActiveImages() > 0); + } else { + shadowRenderer = null; + } + } else { + this.shadowClearPasses = ImmutableList.of(); + this.shadowClearPassesFull = ImmutableList.of(); + this.shadowRenderer = null; + } + + this.clearPassesFull = ClearPassCreator.createClearPasses(renderTargets, true, programs.getPackDirectives().getRenderTargetDirectives()); + this.clearPasses = ClearPassCreator.createClearPasses(renderTargets, false, programs.getPackDirectives().getRenderTargetDirectives()); + + // SodiumTerrainPipeline setup follows. + + Supplier> flipped = () -> isBeforeTranslucent ? flippedAfterPrepare : flippedAfterTranslucent; + + IntFunction createTerrainSamplers = (programId) -> { + ProgramSamplers.Builder builder = ProgramSamplers.builder(programId, IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS); + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureManager.getCustomTextureIdMap(TextureStage.GBUFFERS_AND_SHADOW)); + + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, flipped, renderTargets, false); + IrisSamplers.addLevelSamplers(customTextureSamplerInterceptor, this, whitePixel, new InputAvailability(true, true, false)); + IrisSamplers.addWorldDepthSamplers(customTextureSamplerInterceptor, renderTargets); + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, customTextureManager.getNoiseTexture()); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, Objects.requireNonNull(shadowRenderTargets)); + } + + return builder.build(); + }; + + IntFunction createTerrainImages = (programId) -> { + ProgramImages.Builder builder = ProgramImages.builder(programId); + + IrisImages.addRenderTargetImages(builder, flipped, renderTargets); + + if (IrisImages.hasShadowImages(builder)) { + IrisImages.addShadowColorImages(builder, Objects.requireNonNull(shadowRenderTargets)); + } + + return builder.build(); + }; + + IntFunction createShadowTerrainSamplers = (programId) -> { + ProgramSamplers.Builder builder = ProgramSamplers.builder(programId, IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS); + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureManager.getCustomTextureIdMap(TextureStage.GBUFFERS_AND_SHADOW)); + + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flippedAfterPrepare, renderTargets, false); + IrisSamplers.addLevelSamplers(customTextureSamplerInterceptor, this, whitePixel, new InputAvailability(true, true, false)); + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, customTextureManager.getNoiseTexture()); + + // Only initialize these samplers if the shadow map renderer exists. Otherwise, this program shouldn't be used at all? + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, Objects.requireNonNull(shadowRenderTargets)); + } + + return builder.build(); + }; + IntFunction createShadowTerrainImages = (programId) -> { + ProgramImages.Builder builder = ProgramImages.builder(programId); + + IrisImages.addRenderTargetImages(builder, () -> flippedAfterPrepare, renderTargets); + + if (IrisImages.hasShadowImages(builder)) { + IrisImages.addShadowColorImages(builder, Objects.requireNonNull(shadowRenderTargets)); + } + + return builder.build(); + }; + this.sodiumTerrainPipeline = new SodiumTerrainPipeline(this, programs, createTerrainSamplers, + shadowRenderer == null ? null : createShadowTerrainSamplers, createTerrainImages, + shadowRenderer == null ? null : createShadowTerrainImages); + } + + private RenderTargets getRenderTargets() { + return renderTargets; + } + + private void checkWorld() { + // If we're not in a world, then obviously we cannot possibly be rendering a world. + if (Minecraft.getMinecraft().theWorld == null) { + isRenderingWorld = false; + current = null; + } + } + + @Override + public boolean shouldDisableVanillaEntityShadows() { + // OptiFine seems to disable vanilla shadows when the shaderpack uses shadow mapping? + return shadowRenderer != null; + } + + @Override + public boolean shouldDisableDirectionalShading() { + return !oldLighting; + } + + @Override + public CloudSetting getCloudSetting() { + return cloudSetting; + } + + @Override + public boolean shouldRenderUnderwaterOverlay() { + return shouldRenderUnderwaterOverlay; + } + + @Override + public boolean shouldRenderVignette() { + return shouldRenderVignette; + } + + @Override + public boolean shouldRenderSun() { + return shouldRenderSun; + } + + @Override + public boolean shouldRenderMoon() { + return shouldRenderMoon; + } + + @Override + public boolean shouldWriteRainAndSnowToDepthBuffer() { + return shouldWriteRainAndSnowToDepthBuffer; + } + + @Override + public boolean shouldRenderParticlesBeforeDeferred() { + return shouldRenderParticlesBeforeDeferred; + } + + @Override + public boolean allowConcurrentCompute() { + return allowConcurrentCompute; + } + + @Override + public float getSunPathRotation() { + return sunPathRotation; + } + + private RenderCondition getCondition(WorldRenderingPhase phase) { + if (isRenderingShadow) { + return RenderCondition.SHADOW; + } + + if (special != null) { + if (special == SpecialCondition.BEACON_BEAM) { + return RenderCondition.BEACON_BEAM; + } else if (special == SpecialCondition.ENTITY_EYES) { + return RenderCondition.ENTITY_EYES; + } else if (special == SpecialCondition.GLINT) { + return RenderCondition.GLINT; + } + } + + switch (phase) { + case NONE, OUTLINE, DEBUG, PARTICLES: + return RenderCondition.DEFAULT; + case SKY, SUNSET, CUSTOM_SKY, SUN, MOON, STARS, VOID: + return RenderCondition.SKY; + case TERRAIN_SOLID, TERRAIN_CUTOUT, TERRAIN_CUTOUT_MIPPED: + return RenderCondition.TERRAIN_OPAQUE; + case ENTITIES: + if (GLStateManager.getBlendState().srcRgb == SRC_ALPHA && + GLStateManager.getBlendState().srcAlpha == ONE_MINUS_SRC_ALPHA && + GLStateManager.getBlendState().dstRgb == ONE && + GLStateManager.getBlendState().dstAlpha == ONE_MINUS_SRC_ALPHA) + { + return RenderCondition.ENTITIES_TRANSLUCENT; + } else { + return RenderCondition.ENTITIES; + } + case BLOCK_ENTITIES: + return RenderCondition.BLOCK_ENTITIES; + case DESTROY: + return RenderCondition.DESTROY; + case HAND_SOLID: + return RenderCondition.HAND_OPAQUE; + case TERRAIN_TRANSLUCENT, TRIPWIRE: + return RenderCondition.TERRAIN_TRANSLUCENT; + case CLOUDS: + return RenderCondition.CLOUDS; + case RAIN_SNOW: + return RenderCondition.RAIN_SNOW; + case HAND_TRANSLUCENT: + return RenderCondition.HAND_TRANSLUCENT; + case WORLD_BORDER: + return RenderCondition.WORLD_BORDER; + default: + throw new IllegalStateException("Unknown render phase " + phase); + } + } + + private void matchPass() { + if (!isRenderingWorld || isRenderingFullScreenPass || isPostChain || !isMainBound) { + return; + } + + if (sodiumTerrainRendering) { + beginPass(table.match(getCondition(getPhase()), new InputAvailability(true, true, false))); + return; + } + + beginPass(table.match(getCondition(getPhase()), inputs)); + } + + public void beginPass(Pass pass) { + if (current == pass) { + return; + } + + if (current != null) { + current.stopUsing(); + } + + current = pass; + + if (pass != null) { + pass.use(); + } else { + Program.unbind(); + } + } + + private Pass createDefaultPass() { + final GlFramebuffer framebufferBeforeTranslucents = renderTargets.createGbufferFramebuffer(flippedAfterPrepare, new int[] {0}); + final GlFramebuffer framebufferAfterTranslucents = renderTargets.createGbufferFramebuffer(flippedAfterTranslucent, new int[] {0}); + + + return new Pass(null, framebufferBeforeTranslucents, framebufferAfterTranslucents, null, + null, Collections.emptyList(), false); + } + + private Pass createPass(ProgramSource source, InputAvailability availability, boolean shadow, ProgramId id) { + // TODO: Properly handle empty shaders? + Map transformed = TransformPatcher.patchAttributes( + source.getVertexSource().orElseThrow(NullPointerException::new), + source.getGeometrySource().orElse(null), + source.getFragmentSource().orElseThrow(NullPointerException::new), + availability); + String vertex = transformed.get(PatchShaderType.VERTEX); + String geometry = transformed.get(PatchShaderType.GEOMETRY); + String fragment = transformed.get(PatchShaderType.FRAGMENT); + + PatchedShaderPrinter.debugPatchedShaders(source.getName(), vertex, geometry, fragment); + + ProgramBuilder builder = ProgramBuilder.begin(source.getName(), vertex, geometry, fragment, + IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS); + + return createPassInner(builder, source.getParent().getPack().getIdMap(), source.getDirectives(), source.getParent().getPackDirectives(), availability, shadow, id); + } + + private Pass createPassInner(ProgramBuilder builder, IdMap map, ProgramDirectives programDirectives, + PackDirectives packDirectives, InputAvailability availability, boolean shadow, ProgramId id) { + + CommonUniforms.addCommonUniforms(builder, map, packDirectives, updateNotifier); + + Supplier> flipped; + + if (shadow) { + flipped = () -> (shouldRenderPrepareBeforeShadow ? flippedAfterPrepare : flippedBeforeShadow); + } else { + flipped = () -> isBeforeTranslucent ? flippedAfterPrepare : flippedAfterTranslucent; + } + + TextureStage textureStage = TextureStage.GBUFFERS_AND_SHADOW; + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = + ProgramSamplers.customTextureSamplerInterceptor(builder, + customTextureManager.getCustomTextureIdMap(textureStage)); + + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, flipped, renderTargets, false); + IrisImages.addRenderTargetImages(builder, flipped, renderTargets); + + if (!shouldBindPBR) { + shouldBindPBR = IrisSamplers.hasPBRSamplers(customTextureSamplerInterceptor); + } + + IrisSamplers.addLevelSamplers(customTextureSamplerInterceptor, this, whitePixel, availability); + + if (!shadow) { + IrisSamplers.addWorldDepthSamplers(customTextureSamplerInterceptor, renderTargets); + } + + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, customTextureManager.getNoiseTexture()); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + if (!shadow) { + shadowTargetsSupplier.get(); + } + + if (shadowRenderTargets != null) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowRenderTargets); + IrisImages.addShadowColorImages(builder, shadowRenderTargets); + } + } + + GlFramebuffer framebufferBeforeTranslucents; + GlFramebuffer framebufferAfterTranslucents; + + if (shadow) { + // Always add both draw buffers on the shadow pass. + framebufferBeforeTranslucents = shadowTargetsSupplier.get().createShadowFramebuffer(shadowRenderTargets.snapshot(), new int[] { 0, 1 }); + framebufferAfterTranslucents = framebufferBeforeTranslucents; + } else { + framebufferBeforeTranslucents = renderTargets.createGbufferFramebuffer(flippedAfterPrepare, programDirectives.getDrawBuffers()); + framebufferAfterTranslucents = renderTargets.createGbufferFramebuffer(flippedAfterTranslucent, programDirectives.getDrawBuffers()); + } + + builder.bindAttributeLocation(11, "mc_Entity"); + builder.bindAttributeLocation(12, "mc_midTexCoord"); + builder.bindAttributeLocation(13, "at_tangent"); + builder.bindAttributeLocation(14, "at_midBlock"); + + AlphaTestOverride alphaTestOverride = programDirectives.getAlphaTestOverride().orElse(null); + + List bufferOverrides = new ArrayList<>(); + + programDirectives.getBufferBlendOverrides().forEach(information -> { + int index = Ints.indexOf(programDirectives.getDrawBuffers(), information.getIndex()); + if (index > -1) { + bufferOverrides.add(new BufferBlendOverride(index, information.getBlendMode())); + } + }); + + return new Pass(builder.build(), framebufferBeforeTranslucents, framebufferAfterTranslucents, alphaTestOverride, + programDirectives.getBlendModeOverride().orElse(id.getBlendModeOverride()), bufferOverrides, shadow); + } + + private boolean isPostChain; + private boolean isMainBound = true; + + @Override + public void beginPostChain() { + isPostChain = true; + + beginPass(null); + } + + @Override + public void endPostChain() { + isPostChain = false; + } + + @Override + public void setIsMainBound(boolean bound) { + isMainBound = bound; + + if (!isRenderingWorld || isRenderingFullScreenPass || isPostChain) { + return; + } + + if (bound) { + // force refresh + current = null; + } else { + beginPass(null); + } + } + + private final class Pass { + @Nullable + private final Program program; + private final GlFramebuffer framebufferBeforeTranslucents; + private final GlFramebuffer framebufferAfterTranslucents; + @Nullable + private final AlphaTestOverride alphaTestOverride; + @Nullable + private final BlendModeOverride blendModeOverride; + @Nullable + private final List bufferBlendOverrides; + private final boolean shadowViewport; + + private Pass(@Nullable Program program, GlFramebuffer framebufferBeforeTranslucents, GlFramebuffer framebufferAfterTranslucents, + @Nullable AlphaTestOverride alphaTestOverride, @Nullable BlendModeOverride blendModeOverride, @Nullable List bufferBlendOverrides, boolean shadowViewport) { + this.program = program; + this.framebufferBeforeTranslucents = framebufferBeforeTranslucents; + this.framebufferAfterTranslucents = framebufferAfterTranslucents; + this.alphaTestOverride = alphaTestOverride; + this.blendModeOverride = blendModeOverride; + this.bufferBlendOverrides = bufferBlendOverrides; + this.shadowViewport = shadowViewport; + } + + public void use() { + if (isBeforeTranslucent) { + framebufferBeforeTranslucents.bind(); + } else { + framebufferAfterTranslucents.bind(); + } + + if (shadowViewport) { + GL11.glViewport(0, 0, shadowMapResolution, shadowMapResolution); + } else { + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + GL11.glViewport(0, 0, main.framebufferWidth, main.framebufferHeight); + } + + if (program != null && !sodiumTerrainRendering) { + program.use(); + } + + if (alphaTestOverride != null) { + alphaTestOverride.apply(); + } else { + // Previous program on the stack might have applied an override + AlphaTestOverride.restore(); + } + + if (blendModeOverride != null) { + blendModeOverride.apply(); + } else { + // Previous program on the stack might have applied an override + BlendModeOverride.restore(); + } + + if (bufferBlendOverrides != null && !bufferBlendOverrides.isEmpty()) { + bufferBlendOverrides.forEach(BufferBlendOverride::apply); + } + } + + public void stopUsing() { + if (alphaTestOverride != null) { + AlphaTestOverride.restore(); + } + + if (blendModeOverride != null || (bufferBlendOverrides != null && !bufferBlendOverrides.isEmpty())) { + BlendModeOverride.restore(); + } + } + + @Nullable + public Program getProgram() { + return program; + } + + public void destroy() { + if (this.program != null) { + this.program.destroy(); + } + } + } + + @Override + public void destroy() { + BlendModeOverride.restore(); + AlphaTestOverride.restore(); + + destroyPasses(table); + + // Destroy the composite rendering pipeline + // + // This destroys all the loaded composite programs as well. + compositeRenderer.destroy(); + deferredRenderer.destroy(); + finalPassRenderer.destroy(); + centerDepthSampler.destroy(); + + horizonRenderer.destroy(); + + // Make sure that any custom framebuffers are not bound before destroying render targets + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, 0); + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_DRAW_FRAMEBUFFER, 0); + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, 0); + + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false); + + // Destroy our render targets + // + // While it's possible to just clear them instead and reuse them, we'd need to investigate whether or not this + // would help performance. + renderTargets.destroy(); + + // destroy the shadow render targets + if (shadowRenderTargets != null) { + shadowRenderTargets.destroy(); + } + + // Destroy custom textures and the static samplers (normals, specular, and noise) + customTextureManager.destroy(); +// whitePixel.releaseId(); + } + + private static void destroyPasses(ProgramTable table) { + Set destroyed = new HashSet<>(); + + table.forEach(pass -> { + if (pass == null) { + return; + } + + if (destroyed.contains(pass)) { + return; + } + + pass.destroy(); + destroyed.add(pass); + }); + } + + private void prepareRenderTargets() { + // Make sure we're using texture unit 0 for this. + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + final Vector4f emptyClearColor = new Vector4f(1.0F); + + if (shadowRenderTargets != null) { + if (packDirectives.getShadowDirectives().isShadowEnabled() == OptionalBoolean.FALSE) { + if (shadowRenderTargets.isFullClearRequired()) { + shadowRenderTargets.onFullClear(); + for (ClearPass clearPass : shadowClearPassesFull) { + clearPass.execute(emptyClearColor); + } + } + } else { + // Clear depth first, regardless of any color clearing. + shadowRenderTargets.getDepthSourceFb().bind(); + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + if (Minecraft.isRunningOnMac) { + GL11.glGetError(); + } + + + ImmutableList passes; + + for (ComputeProgram computeProgram : shadowComputes) { + if (computeProgram != null) { + computeProgram.dispatch(shadowMapResolution, shadowMapResolution); + } + } + + if (shadowRenderTargets.isFullClearRequired()) { + passes = shadowClearPassesFull; + shadowRenderTargets.onFullClear(); + } else { + passes = shadowClearPasses; + } + + for (ClearPass clearPass : passes) { + clearPass.execute(emptyClearColor); + } + } + } + + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + + final int depthTextureId = ((IRenderTargetExt)main).getIris$depthTextureId(); + final int internalFormat = TextureInfoCache.INSTANCE.getInfo(depthTextureId).getInternalFormat(); + final DepthBufferFormat depthBufferFormat = DepthBufferFormat.fromGlEnumOrDefault(internalFormat); + + final boolean changed = renderTargets.resizeIfNeeded(((IRenderTargetExt)main).iris$getDepthBufferVersion(), depthTextureId, main.framebufferWidth, + main.framebufferHeight, depthBufferFormat, packDirectives); + + if (changed) { + prepareRenderer.recalculateSizes(); + deferredRenderer.recalculateSizes(); + compositeRenderer.recalculateSizes(); + finalPassRenderer.recalculateSwapPassSize(); + + this.clearPassesFull.forEach(clearPass -> renderTargets.destroyFramebuffer(clearPass.getFramebuffer())); + this.clearPasses.forEach(clearPass -> renderTargets.destroyFramebuffer(clearPass.getFramebuffer())); + + this.clearPassesFull = ClearPassCreator.createClearPasses(renderTargets, true, packDirectives.getRenderTargetDirectives()); + this.clearPasses = ClearPassCreator.createClearPasses(renderTargets, false, packDirectives.getRenderTargetDirectives()); + } + + final ImmutableList passes; + + if (renderTargets.isFullClearRequired()) { + renderTargets.onFullClear(); + passes = clearPassesFull; + } else { + passes = clearPasses; + } + + final Vector3d fogColor3 = GLStateManager.getFogColor(); + + // NB: The alpha value must be 1.0 here, or else you will get a bunch of bugs. Sildur's Vibrant Shaders + // will give you pink reflections and other weirdness if this is zero. + final Vector4f fogColor = new Vector4f((float) fogColor3.x, (float) fogColor3.y, (float) fogColor3.z, 1.0F); + + for (ClearPass clearPass : passes) { + clearPass.execute(fogColor); + } + + // Reset framebuffer and viewport + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(true); + } + + private ComputeProgram[] createShadowComputes(ComputeSource[] compute, ProgramSet programSet) { + ComputeProgram[] programs = new ComputeProgram[compute.length]; + for (int i = 0; i < programs.length; i++) { + ComputeSource source = compute[i]; + if (source == null || !source.getSource().isPresent()) { + continue; + } else { + ProgramBuilder builder; + + try { + builder = ProgramBuilder.beginCompute(source.getName(), source.getSource().orElse(null), IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS); + } catch (RuntimeException e) { + // TODO: Better error handling + throw new RuntimeException("Shader compilation failed!", e); + } + + CommonUniforms.addCommonUniforms(builder, programSet.getPack().getIdMap(), programSet.getPackDirectives(), updateNotifier); + + Supplier> flipped; + + flipped = () -> flippedBeforeShadow; + + TextureStage textureStage = TextureStage.GBUFFERS_AND_SHADOW; + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = + ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureManager.getCustomTextureIdMap(textureStage)); + + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, flipped, renderTargets, false); + IrisImages.addRenderTargetImages(builder, flipped, renderTargets); + + IrisSamplers.addLevelSamplers(customTextureSamplerInterceptor, this, whitePixel, new InputAvailability(true, true, false)); + + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, customTextureManager.getNoiseTexture()); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + if (shadowRenderTargets != null) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowRenderTargets); + IrisImages.addShadowColorImages(builder, shadowRenderTargets); + } + } + + programs[i] = builder.buildCompute(); + + programs[i].setWorkGroupInfo(source.getWorkGroupRelative(), source.getWorkGroups()); + } + } + + + return programs; + } + + @Override + public void beginHand() { + // We need to copy the current depth texture so that depthtex2 can contain the depth values for + // all non-translucent content without the hand, as required. + renderTargets.copyPreHandDepth(); + } + + @Override + public void beginTranslucents() { + isBeforeTranslucent = false; + + // We need to copy the current depth texture so that depthtex1 can contain the depth values for + // all non-translucent content, as required. + renderTargets.copyPreTranslucentDepth(); + + + // needed to remove blend mode overrides and similar + beginPass(null); + + isRenderingFullScreenPass = true; + + deferredRenderer.renderAll(); + + GLStateManager.enableBlend(); + GLStateManager.enableAlphaTest(); + + // note: we are careful not to touch the lightmap texture unit or overlay color texture unit here, + // so we don't need to do anything to restore them if needed. + // + // Previous versions of the code tried to "restore" things by enabling the lightmap & overlay color + // but that actually broke rendering of clouds and rain by making them appear red in the case of + // a pack not overriding those shader programs. + // + // Not good! + + isRenderingFullScreenPass = false; + } + + @Override + public void renderShadows(EntityRenderer levelRenderer, Camera playerCamera) { + if (shouldRenderPrepareBeforeShadow) { + isRenderingFullScreenPass = true; + + prepareRenderer.renderAll(); + + isRenderingFullScreenPass = false; + } + + if (shadowRenderer != null) { + isRenderingShadow = true; + + shadowRenderer.renderShadows(levelRenderer, playerCamera); + + // needed to remove blend mode overrides and similar + beginPass(null); + isRenderingShadow = false; + } + + if (!shouldRenderPrepareBeforeShadow) { + isRenderingFullScreenPass = true; + + prepareRenderer.renderAll(); + + isRenderingFullScreenPass = false; + } + } + + @Override + public void addDebugText(List messages) { + messages.add(""); + + if (shadowRenderer != null) { + shadowRenderer.addDebugText(messages); + } else { + messages.add("[" + Iris.MODNAME + "] Shadow Maps: not used by shader pack"); + } + } + + @Override + public OptionalInt getForcedShadowRenderDistanceChunksForDisplay() { + return forcedShadowRenderDistanceChunks; + } + + // TODO: better way to avoid this global state? + private boolean isRenderingWorld = false; + private boolean isRenderingFullScreenPass = false; + + @Override + public void beginLevelRendering() { + isRenderingFullScreenPass = false; + isRenderingWorld = true; + isBeforeTranslucent = true; + isMainBound = true; + isPostChain = false; + phase = WorldRenderingPhase.NONE; + overridePhase = null; +// HandRenderer.INSTANCE.getBufferSource().resetDrawCalls(); + + checkWorld(); + + if (!isRenderingWorld) { + Iris.logger.warn("beginWorldRender was called but we are not currently rendering a world?"); + return; + } + + if (current != null) { + throw new IllegalStateException("Called beginLevelRendering but level rendering appears to still be in progress?"); + } + + updateNotifier.onNewFrame(); + + // Get ready for world rendering + prepareRenderTargets(); + + setPhase(WorldRenderingPhase.SKY); + + // Render our horizon box before actual sky rendering to avoid being broken by mods that do weird things + // while rendering the sky. + // + // A lot of dimension mods touch sky rendering, FabricSkyboxes injects at HEAD and cancels, etc. +// DimensionSpecialEffects.SkyType skyType = Minecraft.getMinecraft().theWorld.effects().skyType(); + + if (true/*skyType == DimensionSpecialEffects.SkyType.NORMAL*/) { + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDepthMask(false); + + final Vector3d fogColor = GLStateManager.getFogColor(); + GL11.glColor4f((float) fogColor.x, (float) fogColor.y, (float) fogColor.z, 1.0F); + + horizonRenderer.renderHorizon(RenderingState.INSTANCE.getModelViewBuffer()); + + GL11.glDepthMask(true); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + } + + @Override + public void finalizeLevelRendering() { + checkWorld(); + + if (!isRenderingWorld) { + Iris.logger.warn("finalizeWorldRendering was called but we are not currently rendering a world?"); + return; + } + + beginPass(null); + + isRenderingWorld = false; + phase = WorldRenderingPhase.NONE; + overridePhase = null; + + isRenderingFullScreenPass = true; + + centerDepthSampler.sampleCenterDepth(); + + compositeRenderer.renderAll(); + finalPassRenderer.renderFinalPass(); + + isRenderingFullScreenPass = false; + } + + @Override + public SodiumTerrainPipeline getSodiumTerrainPipeline() { + return sodiumTerrainPipeline; + } + + @Override + public FrameUpdateNotifier getFrameUpdateNotifier() { + return updateNotifier; + } + + @Override + public WorldRenderingPhase getPhase() { + if (overridePhase != null) { + return overridePhase; + } + + return phase; + } + + boolean sodiumTerrainRendering = false; + + @Override + public void syncProgram() { + matchPass(); + } + + @Override + public void beginSodiumTerrainRendering() { + sodiumTerrainRendering = true; + syncProgram(); + + } + + @Override + public void endSodiumTerrainRendering() { + sodiumTerrainRendering = false; + current = null; + syncProgram(); + } + + @Override + public void setOverridePhase(WorldRenderingPhase phase) { + this.overridePhase = phase; + + GbufferPrograms.runPhaseChangeNotifier(); + } + + @Override + public void setPhase(WorldRenderingPhase phase) { + this.phase = phase; + + GbufferPrograms.runPhaseChangeNotifier(); + } + + @Override + public void setInputs(InputAvailability availability) { + this.inputs = availability; + } + + @Override + public void setSpecialCondition(SpecialCondition special) { + this.special = special; + } + + @Override + public RenderTargetStateListener getRenderTargetStateListener() { + return this; + } + + @Override + public int getCurrentNormalTexture() { + return currentNormalTexture; + } + + @Override + public int getCurrentSpecularTexture() { + return currentSpecularTexture; + } + + @Override + public void onBindTexture(int id) { + if (shouldBindPBR && isRenderingWorld) { + PBRTextureHolder pbrHolder = PBRTextureManager.INSTANCE.getOrLoadHolder(id); + currentNormalTexture = pbrHolder.getNormalTexture().getGlTextureId(); + currentSpecularTexture = pbrHolder.getSpecularTexture().getGlTextureId(); + + TextureFormat textureFormat = TextureFormatLoader.getFormat(); + if (textureFormat != null) { + textureFormat.setupTextureParameters(PBRType.NORMAL, pbrHolder.getNormalTexture()); + textureFormat.setupTextureParameters(PBRType.SPECULAR, pbrHolder.getSpecularTexture()); + } + + PBRTextureManager.notifyPBRTexturesChanged(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/FixedFunctionWorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/FixedFunctionWorldRenderingPipeline.java new file mode 100644 index 000000000..efb429135 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/FixedFunctionWorldRenderingPipeline.java @@ -0,0 +1,195 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; +import net.coderbot.iris.gbuffer_overrides.state.RenderTargetStateListener; +import net.coderbot.iris.shaderpack.CloudSetting; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; +import org.lwjgl.opengl.GL20; + +import java.util.List; +import java.util.OptionalInt; + +public class FixedFunctionWorldRenderingPipeline implements WorldRenderingPipeline { + public FixedFunctionWorldRenderingPipeline() { + BlockRenderingSettings.INSTANCE.setDisableDirectionalShading(shouldDisableDirectionalShading()); + BlockRenderingSettings.INSTANCE.setUseSeparateAo(false); + BlockRenderingSettings.INSTANCE.setAmbientOcclusionLevel(1.0f); + BlockRenderingSettings.INSTANCE.setUseExtendedVertexFormat(false); + BlockRenderingSettings.INSTANCE.setBlockTypeIds(null); + } + + @Override + public void beginLevelRendering() { + // Use the default Minecraft framebuffer and ensure that no programs are in use + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(true); + GL20.glUseProgram(0); + } + + @Override + public void renderShadows(EntityRenderer levelRenderer, Camera camera) { + // stub: nothing to do here + } + + @Override + public void addDebugText(List messages) { + // stub: nothing to do here + } + + @Override + public OptionalInt getForcedShadowRenderDistanceChunksForDisplay() { + return OptionalInt.empty(); + } + + @Override + public WorldRenderingPhase getPhase() { + return WorldRenderingPhase.NONE; + } + + @Override + public void beginSodiumTerrainRendering() { + + } + + @Override + public void endSodiumTerrainRendering() { + + } + + @Override + public void setOverridePhase(WorldRenderingPhase phase) { + + } + + @Override + public void setPhase(WorldRenderingPhase phase) { + + } + + @Override + public void setInputs(InputAvailability availability) { + + } + + @Override + public void setSpecialCondition(SpecialCondition special) { + + } + + @Override + public void syncProgram() { + + } + + @Override + public RenderTargetStateListener getRenderTargetStateListener() { + return RenderTargetStateListener.NOP; + } + + @Override + public int getCurrentNormalTexture() { + return 0; + } + + @Override + public int getCurrentSpecularTexture() { + return 0; + } + + @Override + public void onBindTexture(int id) { + + } + + @Override + public void beginHand() { + // stub: nothing to do here + } + + @Override + public void beginTranslucents() { + // stub: nothing to do here + } + + @Override + public void finalizeLevelRendering() { + // stub: nothing to do here + } + + @Override + public void destroy() { + // stub: nothing to do here + } + + @Override + public SodiumTerrainPipeline getSodiumTerrainPipeline() { + // no shaders to override + return null; + } + + @Override + public FrameUpdateNotifier getFrameUpdateNotifier() { + // return a dummy notifier + return new FrameUpdateNotifier(); + } + + @Override + public boolean shouldDisableVanillaEntityShadows() { + return false; + } + + @Override + public boolean shouldDisableDirectionalShading() { + return false; + } + + @Override + public CloudSetting getCloudSetting() { + return CloudSetting.DEFAULT; + } + + @Override + public boolean shouldRenderUnderwaterOverlay() { + return true; + } + + @Override + public boolean shouldRenderVignette() { + return true; + } + + @Override + public boolean shouldRenderSun() { + return true; + } + + @Override + public boolean shouldRenderMoon() { + return true; + } + + @Override + public boolean shouldWriteRainAndSnowToDepthBuffer() { + return false; + } + + @Override + public boolean shouldRenderParticlesBeforeDeferred() { + return false; + } + + @Override + public boolean allowConcurrentCompute() { + return false; + } + + @Override + public float getSunPathRotation() { + // No sun tilt + return 0; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/HandRenderer.java b/src/main/java/net/coderbot/iris/pipeline/HandRenderer.java new file mode 100644 index 000000000..28b100b7c --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/HandRenderer.java @@ -0,0 +1,152 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.mojang.InteractionHand; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import net.irisshaders.iris.api.v0.IrisApi; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemBlock; +import org.joml.Matrix4f; + +public class HandRenderer { + public static final HandRenderer INSTANCE = new HandRenderer(); + + private boolean ACTIVE; + private boolean renderingSolid; + public static final float DEPTH = 0.125F; + + private void setupGlState(RenderGlobal gameRenderer, Camera camera, MatrixStack poseStack, float tickDelta) { +// final MatrixStack.Entry pose = poseStack.peek(); + + // We need to scale the matrix by 0.125 so the hand doesn't clip through blocks. +// Matrix4f scaleMatrix = new Matrix4f().scale(1F, 1F, DEPTH); + // TODO: ProjectionMatrix +// scaleMatrix.multiply(gameRenderer.getProjectionMatrix(camera, tickDelta, false)); +// scaleMatrix.mul(projectionMatrix); +// RenderSystem.matrixMode(5889); +// RenderSystem.loadIdentity(); +// RenderSystem.multMatrix(arg); +// RenderSystem.matrixMode(5888); + +// pose.getModel().identity(); +// pose.getNormal().identity(); + +// gameRenderer.invokeBobHurt(poseStack, tickDelta); +// +// if (Minecraft.getMinecraft().gameSettings.viewBobbing) { +//// gameRenderer.invokeBobView(poseStack, tickDelta); +// } + } + + private boolean canRender(Camera camera, RenderGlobal gameRenderer) { + return (camera.isThirdPerson() || !(camera.getEntity() instanceof EntityPlayer) || Minecraft.getMinecraft().gameSettings.hideGUI || (camera.getEntity() instanceof EntityLiving && ((EntityLiving)camera.getEntity()).isPlayerSleeping())); + +// return !(!gameRenderer.getRenderHand() +// || camera.isThirdPerson() +// || !(camera.getEntity() instanceof EntityPlayer) +// || gameRenderer.getPanoramicMode() +// || Minecraft.getMinecraft().gameSettings.hideGUI +// || (camera.getEntity() instanceof EntityLiving && ((EntityLiving)camera.getEntity()).isPlayerSleeping()) +// // TODO: SPECTATOR +// /*|| Minecraft.getMinecraft().gameMode.getPlayerMode() == GameType.SPECTATOR*/); + } + + public boolean isHandTranslucent(InteractionHand hand) { + // TODO: Offhand +// Item item = Minecraft.getMinecraft().thePlayer.getItemBySlot(hand == InteractionHand.OFF_HAND ? EquipmentSlot.OFFHAND : EquipmentSlot.MAINHAND).getItem(); + Item item = Minecraft.getMinecraft().thePlayer.getHeldItem().getItem(); + + if (item instanceof ItemBlock itemBlock) { + // TODO: RenderType +// return ItemBlockRenderTypes.getChunkRenderType(itemBlock.getBlock().defaultBlockState()) == RenderType.translucent(); + return false; + } + + return false; + } + + public boolean isAnyHandTranslucent() { + return isHandTranslucent(InteractionHand.MAIN_HAND) || isHandTranslucent(InteractionHand.OFF_HAND); + } + + public void renderSolid(MatrixStack poseStack, float tickDelta, Camera camera, RenderGlobal gameRenderer, WorldRenderingPipeline pipeline) { + if (!canRender(camera, gameRenderer) || !IrisApi.getInstance().isShaderPackInUse()) { + return; + } + + ACTIVE = true; + + pipeline.setPhase(WorldRenderingPhase.HAND_SOLID); + +// poseStack.push(); + + Minecraft.getMinecraft().mcProfiler.startSection("iris_hand"); + + // TODO: Iris +// setupGlState(gameRenderer, camera, poseStack, tickDelta); + + renderingSolid = true; + // TODO: Hand +// Minecraft.getMinecraft().getItemInHandRenderer().renderHandsWithItems(tickDelta, poseStack, bufferSource, Minecraft.getMinecraft().thePlayer, Minecraft.getMinecraft().getEntityRenderDispatcher().getPackedLightCoords(camera.getEntity(), tickDelta)); + + Minecraft.getMinecraft().mcProfiler.endSection(); + + // TODO: ProjectionMatrix +// gameRenderer.resetProjectionMatrix(CapturedRenderingState.INSTANCE.getGbufferProjection()); + +// poseStack.pop(); + +// bufferSource.endBatch(); + + renderingSolid = false; + + pipeline.setPhase(WorldRenderingPhase.NONE); + + ACTIVE = false; + } + + public void renderTranslucent(MatrixStack poseStack, float tickDelta, Camera camera, RenderGlobal gameRenderer, WorldRenderingPipeline pipeline) { + if (!canRender(camera, gameRenderer) || !isAnyHandTranslucent() || !IrisApi.getInstance().isShaderPackInUse()) { + return; + } + + ACTIVE = true; + + pipeline.setPhase(WorldRenderingPhase.HAND_TRANSLUCENT); + + poseStack.push(); + + Minecraft.getMinecraft().mcProfiler.startSection("iris_hand_translucent"); + + setupGlState(gameRenderer, camera, poseStack, tickDelta); + + // TODO: Hand +// Minecraft.getMinecraft().getItemInHandRenderer().renderHandsWithItems(tickDelta, poseStack, bufferSource, Minecraft.getMinecraft().thePlayer, Minecraft.getMinecraft().getEntityRenderDispatcher().getPackedLightCoords(camera.getEntity(), tickDelta)); + + poseStack.pop(); + + Minecraft.getMinecraft().mcProfiler.endSection(); + + // TODO: ProjectionMatrix +// gameRenderer.resetProjectionMatrix(CapturedRenderingState.INSTANCE.getGbufferProjection()); + +// bufferSource.endBatch(); + + pipeline.setPhase(WorldRenderingPhase.NONE); + + ACTIVE = false; + } + + public boolean isActive() { + return ACTIVE; + } + + public boolean isRenderingSolid() { + return renderingSolid; + } + +} diff --git a/src/main/java/net/coderbot/iris/pipeline/HorizonRenderer.java b/src/main/java/net/coderbot/iris/pipeline/HorizonRenderer.java new file mode 100644 index 000000000..5c98e7af8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/HorizonRenderer.java @@ -0,0 +1,190 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.VertexBuffer; +import com.gtnewhorizons.angelica.mixins.interfaces.ITessellatorInstance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +/** + * Renders the sky horizon. Vanilla Minecraft simply uses the "clear color" for its horizon, and then draws a plane + * above the player. This class extends the sky rendering so that an octagonal prism is drawn around the player instead, + * allowing shaders to perform more advanced sky rendering. + *

+ * However, the horizon rendering is designed so that when sky shaders are not being used, it looks almost exactly the + * same as vanilla sky rendering, except a few almost entirely imperceptible differences where the walls + * of the octagonal prism intersect the top plane. + */ +public class HorizonRenderer { + /** + * The Y coordinate of the top skybox plane. Acts as the upper bound for the horizon prism, since the prism lies + * between the bottom and top skybox planes. + */ + private static final float TOP = 16.0F; + + /** + * The Y coordinate of the bottom skybox plane. Acts as the lower bound for the horizon prism, since the prism lies + * between the bottom and top skybox planes. + */ + private static final float BOTTOM = -16.0F; + + /** + * Cosine of 22.5 degrees. + */ + private static final double COS_22_5 = Math.cos(Math.toRadians(22.5)); + + /** + * Sine of 22.5 degrees. + */ + private static final double SIN_22_5 = Math.sin(Math.toRadians(22.5)); + private VertexBuffer vertexBuffer; + private int currentRenderDistance; + + public HorizonRenderer() { + currentRenderDistance = Minecraft.getMinecraft().gameSettings.renderDistanceChunks; + + rebuildBuffer(); + } + + private void rebuildBuffer() { + if (this.vertexBuffer != null) { + this.vertexBuffer.close(); + } + final Tessellator tessellator = Tessellator.instance; + + // Build the horizon quads into a buffer + tessellator.startDrawingQuads(); //(GL11.GL_QUADS, DefaultVertexFormat.POSITION); + buildHorizon(currentRenderDistance * 16, tessellator); + final ByteBuffer buf = tessellatorToBuffer(tessellator); + ((ITessellatorInstance) tessellator).discard(); + + this.vertexBuffer = new VertexBuffer(); + this.vertexBuffer.bind(); + this.vertexBuffer.upload(buf, tessellator.vertexCount); + this.vertexBuffer.unbind(); + } + + /* Convert the tessellator's data into a buffer that can be uploaded to the GPU. */ + private ByteBuffer tessellatorToBuffer(Tessellator tessellator) { + final int[] rawBuffer = tessellator.rawBuffer; + final int byteSize = (tessellator.vertexCount * 3) << 2; + ByteBuffer byteBuffer = BufferUtils.createByteBuffer(byteSize); + + for(int quadI = 0 ; quadI < tessellator.vertexCount / 4 ; quadI++) { + for(int vertexI = 0 ; vertexI < 4 ; vertexI++) { + int i = (quadI * 4 * 8 ) + (vertexI * 8); + byteBuffer.putFloat(Float.intBitsToFloat(rawBuffer[i + 0])); + byteBuffer.putFloat(Float.intBitsToFloat(rawBuffer[i + 1])); + byteBuffer.putFloat(Float.intBitsToFloat(rawBuffer[i + 2])); + } + } + + return (ByteBuffer) byteBuffer.rewind(); + } + + private void buildQuad(Tessellator consumer, double x1, double z1, double x2, double z2) { + consumer.addVertex(x1, BOTTOM, z1); + consumer.addVertex(x1, TOP, z1); + consumer.addVertex(x2, TOP, z2); + consumer.addVertex(x2, BOTTOM, z2); + } + + private void buildHalf(Tessellator consumer, double adjacent, double opposite, boolean invert) { + if (invert) { + adjacent = -adjacent; + opposite = -opposite; + } + + // NB: Make sure that these vertices are being specified in counterclockwise order! + // Otherwise back face culling will remove your quads, and you'll be wondering why there's a hole in your horizon. + // Don't poke holes in the horizon. Specify vertices in counterclockwise order. + + // +X,-Z face + buildQuad(consumer, adjacent, -opposite, opposite, -adjacent); + // +X face + buildQuad(consumer, adjacent, opposite, adjacent, -opposite); + // +X,+Z face + buildQuad(consumer, opposite, adjacent, adjacent, opposite); + // +Z face + buildQuad(consumer, -opposite, adjacent, opposite, adjacent); + } + + /** + * @param adjacent the adjacent side length of the a triangle with a hypotenuse extending from the center of the + * octagon to a given vertex on the perimeter. + * @param opposite the opposite side length of the a triangle with a hypotenuse extending from the center of the + * octagon to a given vertex on the perimeter. + */ + private void buildOctagonalPrism(Tessellator consumer, double adjacent, double opposite) { + buildHalf(consumer, adjacent, opposite, false); + buildHalf(consumer, adjacent, opposite, true); + } + + private void buildRegularOctagonalPrism(Tessellator consumer, double radius) { + buildOctagonalPrism(consumer, radius * COS_22_5, radius * SIN_22_5); + } + + private void buildBottomPlane(Tessellator consumer, int radius) { + for (int x = -radius; x <= radius; x += 64) { + for (int z = -radius; z <= radius; z += 64) { + consumer.addVertex(x + 64, BOTTOM, z); + consumer.addVertex(x, BOTTOM, z); + consumer.addVertex(x, BOTTOM, z + 64); + consumer.addVertex(x + 64, BOTTOM, z + 64); + } + } + } + + private void buildTopPlane(Tessellator consumer, int radius) { + // You might be tempted to try to combine this with buildBottomPlane to avoid code duplication, + // but that won't work since the winding order has to be reversed or else one of the planes will be + // discarded by back face culling. + for (int x = -radius; x <= radius; x += 64) { + for (int z = -radius; z <= radius; z += 64) { + consumer.addVertex(x + 64, TOP, z); + consumer.addVertex(x + 64, TOP, z + 64); + consumer.addVertex(x, TOP, z + 64); + consumer.addVertex(x, TOP, z); + } + } + } + + private void buildHorizon(int radius, Tessellator consumer) { + if (radius > 256) { + // Prevent the prism from getting too large, this causes issues on some shader packs that modify the vanilla + // sky if we don't do this. + radius = 256; + } + + buildRegularOctagonalPrism(consumer, radius); + + // Replicate the vanilla top plane since we can't assume that it'll be rendered. + // TODO: Remove vanilla top plane + buildTopPlane(consumer, 384); + + // Always make the bottom plane have a radius of 384, to match the top plane. + buildBottomPlane(consumer, 384); + } + + public void renderHorizon(FloatBuffer floatBuffer) { + if (currentRenderDistance != Minecraft.getMinecraft().gameSettings.renderDistanceChunks) { + currentRenderDistance = Minecraft.getMinecraft().gameSettings.renderDistanceChunks; + rebuildBuffer(); + } + + vertexBuffer.bind(); + GL11.glVertexPointer(3, GL11.GL_FLOAT, 12, 0L); + GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY); + vertexBuffer.draw(floatBuffer, GL11.GL_QUADS); + GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY); + vertexBuffer.unbind(); + } + + public void destroy() { + vertexBuffer.close(); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/PatchedShaderPrinter.java b/src/main/java/net/coderbot/iris/pipeline/PatchedShaderPrinter.java new file mode 100644 index 000000000..ee8a4ff89 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/PatchedShaderPrinter.java @@ -0,0 +1,70 @@ +package net.coderbot.iris.pipeline; + +import net.coderbot.iris.Iris; +import net.minecraft.client.Minecraft; +import net.minecraft.launchwrapper.Launch; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** + * Static class that deals with printing the patched_shader folder. + */ +public class PatchedShaderPrinter { + private static boolean outputLocationCleared = false; + private static int programCounter = 0; + + // Is this too early? We'll find out! + public static final boolean prettyPrintShaders = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment") + || System.getProperty("iris.prettyPrintShaders", "false").equals("true"); + + public static void resetPrintState() { + outputLocationCleared = false; + programCounter = 0; + } + + public static void debugPatchedShaders(String name, String vertex, String geometry, String fragment) { + if (prettyPrintShaders) { + final Path debugOutDir = Minecraft.getMinecraft().mcDataDir.toPath().resolve("patched_shaders"); + if (!outputLocationCleared) { + try { + if (Files.exists(debugOutDir)) { + try (Stream stream = Files.list(debugOutDir)) { + stream.forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + + Files.createDirectories(debugOutDir); + } catch (IOException e) { + Iris.logger.warn("Failed to initialize debug patched shader source location", e); + } + outputLocationCleared = true; + } + + try { + programCounter++; + String prefix = String.format("%03d_", programCounter); + if (vertex != null) { + Files.write(debugOutDir.resolve(prefix + name + ".vsh"), vertex.getBytes(StandardCharsets.UTF_8)); + } + if (geometry != null) { + Files.write(debugOutDir.resolve(prefix + name + ".gsh"), geometry.getBytes(StandardCharsets.UTF_8)); + } + if (fragment != null) { + Files.write(debugOutDir.resolve(prefix + name + ".fsh"), fragment.getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + Iris.logger.warn("Failed to write debug patched shader source", e); + } + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/PipelineManager.java b/src/main/java/net/coderbot/iris/pipeline/PipelineManager.java new file mode 100644 index 000000000..987419f9b --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/PipelineManager.java @@ -0,0 +1,106 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.shaderpack.DimensionId; +import net.coderbot.iris.uniforms.SystemTimeUniforms; +import net.minecraft.client.Minecraft; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +public class PipelineManager { + private static PipelineManager instance; + private final Function pipelineFactory; + private final Map pipelinesPerDimension = new HashMap<>(); + private WorldRenderingPipeline pipeline = new FixedFunctionWorldRenderingPipeline(); + @Getter + private int versionCounterForSodiumShaderReload = 0; + + public PipelineManager(Function pipelineFactory) { + this.pipelineFactory = pipelineFactory; + } + + public WorldRenderingPipeline preparePipeline(DimensionId currentDimension) { + if (!pipelinesPerDimension.containsKey(currentDimension)) { + SystemTimeUniforms.COUNTER.reset(); + SystemTimeUniforms.TIMER.reset(); + + Iris.logger.info("Creating pipeline for dimension {}", currentDimension); + pipeline = pipelineFactory.apply(currentDimension); + pipelinesPerDimension.put(currentDimension, pipeline); + + if (BlockRenderingSettings.INSTANCE.isReloadRequired()) { + if (Minecraft.getMinecraft().renderGlobal != null) { + // TODO: Iris +// Minecraft.getMinecraft().renderGlobal.allChanged(); + } + + BlockRenderingSettings.INSTANCE.clearReloadRequired(); + } + } else { + pipeline = pipelinesPerDimension.get(currentDimension); + } + + return pipeline; + } + + @Nullable + public WorldRenderingPipeline getPipelineNullable() { + return pipeline; + } + + public Optional getPipeline() { + return Optional.ofNullable(pipeline); + } + + + /** + * Destroys all the current pipelines. + * + *

This method is EXTREMELY DANGEROUS! It is a huge potential source of hard-to-trace inconsistencies + * in program state. You must make sure that you immediately re-prepare the pipeline after destroying + * it to prevent the program from falling into an inconsistent state.

+ * + *

In particular,

+ * + * @see this GitHub issue + */ + public void destroyPipeline() { + pipelinesPerDimension.forEach((dimensionId, pipeline) -> { + Iris.logger.info("Destroying pipeline {}", dimensionId); + resetTextureState(); + pipeline.destroy(); + }); + + pipelinesPerDimension.clear(); + pipeline = null; + versionCounterForSodiumShaderReload++; + } + + private void resetTextureState() { + // Unbind all textures + // + // This is necessary because we don't want destroyed render target textures to remain bound to certain texture + // units. Vanilla appears to properly rebind all textures as needed, and we do so too, so this does not cause + // issues elsewhere. + // + // Without this code, there will be weird issues when reloading certain shaderpacks. + for (int i = 0; i < 16; i++) { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + // Set the active texture unit to unit 0 + // + // This seems to be what most code expects. It's a sane default in any case. + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/ShadowRenderer.java b/src/main/java/net/coderbot/iris/pipeline/ShadowRenderer.java new file mode 100644 index 000000000..bc39af666 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/ShadowRenderer.java @@ -0,0 +1,680 @@ +package net.coderbot.iris.pipeline; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.rendering.RenderingState; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shaderpack.PackShadowDirectives; +import net.coderbot.iris.shaderpack.ProgramSource; +import net.coderbot.iris.shadow.ShadowMatrices; +import net.coderbot.iris.shadows.CullingDataCache; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import net.coderbot.iris.shadows.frustum.BoxCuller; +import net.coderbot.iris.shadows.frustum.CullEverythingFrustum; +import net.coderbot.iris.shadows.frustum.FrustumHolder; +import net.coderbot.iris.shadows.frustum.advanced.AdvancedShadowCullingFrustum; +import net.coderbot.iris.shadows.frustum.fallback.BoxCullingFrustum; +import net.coderbot.iris.shadows.frustum.fallback.NonCullingFrustum; +import net.coderbot.iris.uniforms.CameraUniforms; +import net.coderbot.iris.uniforms.CapturedRenderingState; +import net.coderbot.iris.uniforms.CelestialUniforms; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.entity.Entity; +import net.minecraft.profiler.Profiler; +import net.minecraft.tileentity.TileEntity; +import org.joml.Matrix4f; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.ARBTextureSwizzle; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL30; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +public class ShadowRenderer { + public static final Matrix4f MODELVIEW = new Matrix4f(); + public static FloatBuffer MODELVIEW_BUFFER = BufferUtils.createFloatBuffer(16); + public static final Matrix4f PROJECTION = new Matrix4f(); + public static List visibleTileEntities; + public static boolean ACTIVE = false; + private final float halfPlaneLength; + private final float renderDistanceMultiplier; + private final float entityShadowDistanceMultiplier; + private final int resolution; + private final float intervalSize; + private final Float fov; + private final ShadowRenderTargets targets; + private final OptionalBoolean packCullingState; + private boolean packHasVoxelization; + private final boolean shouldRenderTerrain; + private final boolean shouldRenderTranslucent; + private final boolean shouldRenderEntities; + private final boolean shouldRenderPlayer; + private final boolean shouldRenderBlockEntities; + private final float sunPathRotation; +// private final RenderBuffers buffers; +// private final RenderBuffersExt renderBuffersExt; + private final List mipmapPasses = new ArrayList<>(); + private final String debugStringOverall; + private FrustumHolder terrainFrustumHolder; + private FrustumHolder entityFrustumHolder; + private String debugStringTerrain = "(unavailable)"; + private int renderedShadowEntities = 0; + private int renderedShadowTileEntities = 0; + private Profiler profiler; + + public ShadowRenderer(ProgramSource shadow, PackDirectives directives, ShadowRenderTargets shadowRenderTargets) { + + this.profiler = Minecraft.getMinecraft().mcProfiler; + + final PackShadowDirectives shadowDirectives = directives.getShadowDirectives(); + + this.halfPlaneLength = shadowDirectives.getDistance(); + this.renderDistanceMultiplier = shadowDirectives.getDistanceRenderMul(); + this.entityShadowDistanceMultiplier = shadowDirectives.getEntityShadowDistanceMul(); + this.resolution = shadowDirectives.getResolution(); + this.intervalSize = shadowDirectives.getIntervalSize(); + this.shouldRenderTerrain = shadowDirectives.shouldRenderTerrain(); + this.shouldRenderTranslucent = shadowDirectives.shouldRenderTranslucent(); + this.shouldRenderEntities = shadowDirectives.shouldRenderEntities(); + this.shouldRenderPlayer = shadowDirectives.shouldRenderPlayer(); + this.shouldRenderBlockEntities = shadowDirectives.shouldRenderBlockEntities(); + + debugStringOverall = "half plane = " + halfPlaneLength + " meters @ " + resolution + "x" + resolution; + + this.terrainFrustumHolder = new FrustumHolder(); + this.entityFrustumHolder = new FrustumHolder(); + + this.fov = shadowDirectives.getFov(); + this.targets = shadowRenderTargets; + + if (shadow != null) { + // Assume that the shader pack is doing voxelization if a geometry shader is detected. + // Also assume voxelization if image load / store is detected. + this.packHasVoxelization = shadow.getGeometrySource().isPresent(); + this.packCullingState = shadowDirectives.getCullingState(); + } else { + this.packHasVoxelization = false; + this.packCullingState = OptionalBoolean.DEFAULT; + } + + this.sunPathRotation = directives.getSunPathRotation(); + +// this.buffers = new RenderBuffers(); +// +// if (this.buffers instanceof RenderBuffersExt) { +// this.renderBuffersExt = (RenderBuffersExt) buffers; +// } else { +// this.renderBuffersExt = null; +// } + + configureSamplingSettings(shadowDirectives); + } + + public void setUsesImages(boolean usesImages) { + this.packHasVoxelization = packHasVoxelization || usesImages; + } + + public static MatrixStack createShadowModelView(float sunPathRotation, float intervalSize) { + // Determine the camera position + final Vector3d cameraPos = CameraUniforms.getUnshiftedCameraPosition(); + + final double cameraX = cameraPos.x; + final double cameraY = cameraPos.y; + final double cameraZ = cameraPos.z; + + // Set up our modelview matrix stack + final MatrixStack modelView = new MatrixStack(); + ShadowMatrices.createModelViewMatrix(modelView, getShadowAngle(), intervalSize, sunPathRotation, cameraX, cameraY, cameraZ); + + return modelView; + } + + private static WorldClient getLevel() { + return Objects.requireNonNull(Minecraft.getMinecraft().theWorld); + } + + private static float getSkyAngle() { + return Minecraft.getMinecraft().theWorld.getCelestialAngle(CapturedRenderingState.INSTANCE.getTickDelta()); + } + + private static float getSunAngle() { + final float skyAngle = getSkyAngle(); + + if (skyAngle < 0.75F) { + return skyAngle + 0.25F; + } else { + return skyAngle - 0.75F; + } + } + + private static float getShadowAngle() { + float shadowAngle = getSunAngle(); + + if (!CelestialUniforms.isDay()) { + shadowAngle -= 0.5F; + } + + return shadowAngle; + } + + private void configureSamplingSettings(PackShadowDirectives shadowDirectives) { + final ImmutableList depthSamplingSettings = + shadowDirectives.getDepthSamplingSettings(); + + final ImmutableList colorSamplingSettings = + shadowDirectives.getColorSamplingSettings(); + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE4); + + configureDepthSampler(targets.getDepthTexture().getTextureId(), depthSamplingSettings.get(0)); + + configureDepthSampler(targets.getDepthTextureNoTranslucents().getTextureId(), depthSamplingSettings.get(1)); + + for (int i = 0; i < colorSamplingSettings.size(); i++) { + int glTextureId = targets.get(i).getMainTexture(); + + configureSampler(glTextureId, colorSamplingSettings.get(i)); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } + + private final IntBuffer swizzleBuf = BufferUtils.createIntBuffer(4); + private void configureDepthSampler(int glTextureId, PackShadowDirectives.DepthSamplingSettings settings) { + if (settings.getHardwareFiltering()) { + // We have to do this or else shadow hardware filtering breaks entirely! + IrisRenderSystem.texParameteri(glTextureId, GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_COMPARE_MODE, GL30.GL_COMPARE_REF_TO_TEXTURE); + } + + // Workaround for issues with old shader packs like Chocapic v4. + // They expected the driver to put the depth value in z, but it's supposed to only + // be available in r. So we set up the swizzle to fix that. + swizzleBuf.rewind(); + swizzleBuf.put(new int[] { GL11.GL_RED, GL11.GL_RED, GL11.GL_RED, GL11.GL_ONE }).rewind(); + IrisRenderSystem.texParameteriv(glTextureId, GL11.GL_TEXTURE_2D, ARBTextureSwizzle.GL_TEXTURE_SWIZZLE_RGBA, swizzleBuf); + + configureSampler(glTextureId, settings); + } + + private void configureSampler(int glTextureId, PackShadowDirectives.SamplingSettings settings) { + if (settings.getMipmap()) { + final int filteringMode = settings.getNearest() ? GL11.GL_NEAREST_MIPMAP_NEAREST : GL11.GL_LINEAR_MIPMAP_LINEAR; + mipmapPasses.add(new MipmapPass(glTextureId, filteringMode)); + } + + if (!settings.getNearest()) { + // Make sure that things are smoothed + IrisRenderSystem.texParameteri(glTextureId, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(glTextureId, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + } else { + IrisRenderSystem.texParameteri(glTextureId, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(glTextureId, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + } + } + + private void generateMipmaps() { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE4); + + for (MipmapPass mipmapPass : mipmapPasses) { + setupMipmappingForTexture(mipmapPass.getTexture(), mipmapPass.getTargetFilteringMode()); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } + + private void setupMipmappingForTexture(int texture, int filteringMode) { + IrisRenderSystem.generateMipmaps(texture, GL11.GL_TEXTURE_2D); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filteringMode); + } + + private FrustumHolder createShadowFrustum(float renderMultiplier, FrustumHolder holder) { + // TODO: Cull entities / block entities with Advanced Frustum Culling even if voxelization is detected. + String distanceInfo; + String cullingInfo; + if ((packCullingState == OptionalBoolean.FALSE || packHasVoxelization) && packCullingState != OptionalBoolean.TRUE) { + double distance = halfPlaneLength * renderMultiplier; + + String reason; + + if (packCullingState == OptionalBoolean.FALSE) { + reason = "(set by shader pack)"; + } else /*if (packHasVoxelization)*/ { + reason = "(voxelization detected)"; + } + + if (distance <= 0 || distance > Minecraft.getMinecraft().gameSettings.renderDistanceChunks * 16) { + distanceInfo = Minecraft.getMinecraft().gameSettings.renderDistanceChunks * 16 + + " blocks (capped by normal render distance)"; + cullingInfo = "disabled " + reason; + return holder.setInfo(new NonCullingFrustum(), distanceInfo, cullingInfo); + } else { + distanceInfo = distance + " blocks (set by shader pack)"; + cullingInfo = "distance only " + reason; + BoxCuller boxCuller = new BoxCuller(distance); + holder.setInfo(new BoxCullingFrustum(boxCuller), distanceInfo, cullingInfo); + } + } else { + BoxCuller boxCuller; + + double distance = halfPlaneLength * renderMultiplier; + String setter = "(set by shader pack)"; + + if (renderMultiplier < 0) { + // TODO: GUI +// distance = IrisVideoSettings.shadowDistance * 16; + distance = 32 * 16; + setter = "(set by user)"; + } + + if (distance >= Minecraft.getMinecraft().gameSettings.renderDistanceChunks * 16) { + distanceInfo = Minecraft.getMinecraft().gameSettings.renderDistanceChunks * 16 + + " blocks (capped by normal render distance)"; + boxCuller = null; + } else { + distanceInfo = distance + " blocks " + setter; + + if (distance == 0.0) { + cullingInfo = "no shadows rendered"; + holder.setInfo(new CullEverythingFrustum(), distanceInfo, cullingInfo); + } + + boxCuller = new BoxCuller(distance); + } + + cullingInfo = "Advanced Frustum Culling enabled"; + + Vector4f shadowLightPosition = new CelestialUniforms(sunPathRotation).getShadowLightPositionInWorldSpace(); + + Vector3f shadowLightVectorFromOrigin = + new Vector3f(shadowLightPosition.x(), shadowLightPosition.y(), shadowLightPosition.z()); + + shadowLightVectorFromOrigin.normalize(); + + return holder.setInfo(new AdvancedShadowCullingFrustum(RenderingState.INSTANCE.getModelViewMatrix(), RenderingState.INSTANCE.getProjectionMatrix(), + shadowLightVectorFromOrigin, boxCuller), distanceInfo, cullingInfo); + + } + + return holder; + } + + private void setupGlState(Matrix4f projMatrix) { + // Set up our projection matrix and load it into the legacy matrix stack + IrisRenderSystem.setupProjectionMatrix(projMatrix); + + // Disable backface culling + // This partially works around an issue where if the front face of a mountain isn't visible, it casts no + // shadow. + // + // However, it only partially resolves issues of light leaking into caves. + // + // TODO: Better way of preventing light from leaking into places where it shouldn't + GLStateManager.disableCull(); + } + + private void restoreGlState() { + // Restore backface culling + GLStateManager.enableCull(); + + // Make sure to unload the projection matrix + IrisRenderSystem.restoreProjectionMatrix(); + } + + private void copyPreTranslucentDepth() { + profiler.endStartSection("translucent depth copy"); + + targets.copyPreTranslucentDepth(); + } + + private void renderEntities(EntityRenderer levelRenderer, Frustrum frustum, Object bufferSource, MatrixStack modelView, double cameraX, double cameraY, double cameraZ, float tickDelta) { + // TODO: Render +// EntityRenderDispatcher dispatcher = levelRenderer.getEntityRenderDispatcher(); + + int shadowEntities = 0; + + profiler.startSection("cull"); + + List renderedEntities = new ArrayList<>(32); + + // TODO: I'm sure that this can be improved / optimized. + // TODO: Render + // TODO: Entity culling + for (Entity entity : getLevel().loadedEntityList) { + if (false/*!dispatcher.shouldRender(entity, frustum, cameraX, cameraY, cameraZ) || entity.isSpectator()*/) { + continue; + } + + renderedEntities.add(entity); + } + + profiler.endStartSection("sort"); + + // TODO: Render + // Sort the entities by type first in order to allow vanilla's entity batching system to work better. + renderedEntities.sort(Comparator.comparingInt(entity -> entity.getClass().hashCode())); + + profiler.endStartSection("build geometry"); + + // TODO: Render + GL11.glPushMatrix(); + MODELVIEW_BUFFER.clear().rewind(); + modelView.peek().getModel().get(MODELVIEW_BUFFER); + GL11.glLoadMatrix(MODELVIEW_BUFFER); + for (Entity entity : renderedEntities) { + RenderManager.instance.renderEntitySimple(entity, tickDelta);//(entity, cameraX, cameraY, cameraZ, tickDelta, modelView, bufferSource); + shadowEntities++; + } + GL11.glPopMatrix(); + + renderedShadowEntities = shadowEntities; + + profiler.endSection(); + } + + private void renderPlayerEntity(EntityRenderer levelRenderer, Frustrum frustum, Object bufferSource, MatrixStack modelView, double cameraX, double cameraY, double cameraZ, float tickDelta) { + // TODO: Render +// EntityRenderDispatcher dispatcher = levelRenderer.getEntityRenderDispatcher(); + + profiler.startSection("cull"); + + Entity player = Minecraft.getMinecraft().thePlayer; + +// if (!dispatcher.shouldRender(player, frustum, cameraX, cameraY, cameraZ) || player.isSpectator()) { +// return; +// } + + profiler.endStartSection("build geometry"); + + int shadowEntities = 0; + +// if (!player.getPassengers().isEmpty()) { +// for (int i = 0; i < player.getPassengers().size(); i++) { +// levelRenderer.invokeRenderEntity(player.getPassengers().get(i), cameraX, cameraY, cameraZ, tickDelta, modelView, bufferSource); +// shadowEntities++; +// } +// } +// +// if (player.getVehicle() != null) { +// levelRenderer.invokeRenderEntity(player.getVehicle(), cameraX, cameraY, cameraZ, tickDelta, modelView, bufferSource); +// shadowEntities++; +// } +// +// levelRenderer.invokeRenderEntity(player, cameraX, cameraY, cameraZ, tickDelta, modelView, bufferSource); + + shadowEntities++; + + renderedShadowEntities = shadowEntities; + + profiler.endSection(); + } + + private void renderTileEntities(Object bufferSource, MatrixStack modelView, double cameraX, double cameraY, double cameraZ, float partialTicks, boolean hasEntityFrustum) { + profiler.startSection("build blockentities"); + + int shadowTileEntities = 0; + BoxCuller culler = null; + if (hasEntityFrustum) { + culler = new BoxCuller(halfPlaneLength * (renderDistanceMultiplier * entityShadowDistanceMultiplier)); + culler.setPosition(cameraX, cameraY, cameraZ); + } + + for (TileEntity tileEntity : visibleTileEntities) { + if (hasEntityFrustum && (culler.isCulled(tileEntity.xCoord - 1, tileEntity.yCoord - 1, tileEntity.zCoord - 1, tileEntity.xCoord + 1, tileEntity.yCoord + 1, tileEntity.zCoord + 1))) { + continue; + } + modelView.push(); + modelView.translate(tileEntity.xCoord - cameraX, tileEntity.yCoord - cameraY, tileEntity.zCoord - cameraZ); + TileEntityRendererDispatcher.instance.renderTileEntity(tileEntity, partialTicks); + modelView.pop(); + + shadowTileEntities++; + } + + renderedShadowTileEntities = shadowTileEntities; + + profiler.endSection(); + } + + public void renderShadows(EntityRenderer levelRenderer, Camera playerCamera) { + final Minecraft mc = Minecraft.getMinecraft(); + final RenderGlobal rg = mc.renderGlobal; + + // We have to re-query this each frame since this changes based on whether the profiler is active + // If the profiler is inactive, it will return InactiveProfiler.INSTANCE + this.profiler = Minecraft.getMinecraft().mcProfiler; + + Minecraft client = Minecraft.getMinecraft(); + + profiler.endStartSection("shadows"); + ACTIVE = true; + + // NB: We store the previous player buffers in order to be able to allow mods rendering entities in the shadow pass (Flywheel) to use the shadow buffers instead. + // TODO: Render +// RenderBuffers playerBuffers = levelRenderer.getRenderBuffers(); +// levelRenderer.setRenderBuffers(buffers); + + visibleTileEntities = new ArrayList<>(); + + // Create our camera + final MatrixStack modelView = createShadowModelView(this.sunPathRotation, this.intervalSize); + MODELVIEW.set(modelView.peek().getModel()); + + final Matrix4f shadowProjection; + if (this.fov != null) { + // If FOV is not null, the pack wants a perspective based projection matrix. (This is to support legacy packs) + shadowProjection = ShadowMatrices.createPerspectiveMatrix(this.fov); + } else { + shadowProjection = ShadowMatrices.createOrthoMatrix(halfPlaneLength); + } + + PROJECTION.set(shadowProjection); + + profiler.startSection("terrain_setup"); + + if (levelRenderer instanceof CullingDataCache) { + ((CullingDataCache) levelRenderer).saveState(); + } + + profiler.startSection("initialize frustum"); + + terrainFrustumHolder = createShadowFrustum(renderDistanceMultiplier, terrainFrustumHolder); + + // Determine the player camera position + final Vector3d cameraPos = CameraUniforms.getUnshiftedCameraPosition(); + + final double cameraX = cameraPos.x(); + final double cameraY = cameraPos.y(); + final double cameraZ = cameraPos.z(); + + // Center the frustum on the player camera position + terrainFrustumHolder.getFrustum().setPosition(cameraX, cameraY, cameraZ); + + profiler.endSection(); + + // Always schedule a terrain update + // TODO: Only schedule a terrain update if the sun / moon is moving, or the shadow map camera moved. + // We have to ensure that we don't regenerate clouds every frame, since that's what needsUpdate ends up doing. + // This took up to 10% of the frame time before we applied this fix! That's really bad! +// boolean regenerateClouds = levelRenderer.shouldRegenerateClouds(); +// ((LevelRenderer) levelRenderer).needsUpdate(); +// levelRenderer.setShouldRegenerateClouds(regenerateClouds); + + // Execute the vanilla terrain setup / culling routines using our shadow frustum. + mc.renderGlobal.clipRenderersByFrustum(terrainFrustumHolder.getFrustum(), playerCamera.getPartialTicks()); +// levelRenderer.invokeSetupRender(playerCamera, terrainFrustumHolder.getFrustum(), false, levelRenderer.getFrameId(), false); + + // Don't forget to increment the frame counter! This variable is arbitrary and only used in terrain setup, + // and if it's not incremented, the vanilla culling code will get confused and think that it's already seen + // chunks during traversal, and break rendering in concerning ways. +// levelRenderer.setFrameId(levelRenderer.getFrameId() + 1); + + profiler.endStartSection("terrain"); + + setupGlState(PROJECTION); + + // Render all opaque terrain unless pack requests not to + if (shouldRenderTerrain) { + rg.sortAndRender(mc.thePlayer, 0, playerCamera.getPartialTicks()); + +// levelRenderer.invokeRenderChunkLayer(RenderLayer.solid(), modelView, cameraX, cameraY, cameraZ); +// levelRenderer.invokeRenderChunkLayer(RenderLayer.cutout(), modelView, cameraX, cameraY, cameraZ); +// levelRenderer.invokeRenderChunkLayer(RenderLayer.cutoutMipped(), modelView, cameraX, cameraY, cameraZ); + } + + profiler.endStartSection("entities"); + + // Get the current tick delta. Normally this is the same as client.getTickDelta(), but when the game is paused, + // it is set to a fixed value. + final float tickDelta = CapturedRenderingState.INSTANCE.getTickDelta(); + + // Create a constrained shadow frustum for entities to avoid rendering faraway entities in the shadow pass, + // if the shader pack has requested it. Otherwise, use the same frustum as for terrain. + boolean hasEntityFrustum = false; + + if (entityShadowDistanceMultiplier == 1.0F || entityShadowDistanceMultiplier < 0.0F) { + entityFrustumHolder.setInfo(terrainFrustumHolder.getFrustum(), terrainFrustumHolder.getDistanceInfo(), terrainFrustumHolder.getCullingInfo()); + } else { + hasEntityFrustum = true; + entityFrustumHolder = createShadowFrustum(renderDistanceMultiplier * entityShadowDistanceMultiplier, entityFrustumHolder); + } + + Frustrum entityShadowFrustum = entityFrustumHolder.getFrustum(); + entityShadowFrustum.setPosition(cameraX, cameraY, cameraZ); + + // Render nearby entities + // + // Note: We must use a separate BuilderBufferStorage object here, or else very weird things will happen during + // rendering. +// if (renderBuffersExt != null) { +// renderBuffersExt.beginLevelRendering(); +// } + +// if (buffers instanceof DrawCallTrackingRenderBuffers) { +// ((DrawCallTrackingRenderBuffers) buffers).resetDrawCounts(); +// } + +// BufferSource bufferSource = buffers.bufferSource(); + + if (shouldRenderEntities) { + renderEntities(levelRenderer, entityShadowFrustum, null, modelView, cameraX, cameraY, cameraZ, tickDelta); + } else if (shouldRenderPlayer) { +// renderPlayerEntity(levelRenderer, entityShadowFrustum, bufferSource, modelView, cameraX, cameraY, cameraZ, tickDelta); + } + + if (shouldRenderBlockEntities) { +// renderTileEntities(bufferSource, modelView, cameraX, cameraY, cameraZ, tickDelta, hasEntityFrustum); + } + + profiler.endStartSection("draw entities"); + + // NB: Don't try to draw the translucent parts of entities afterwards. It'll cause problems since some + // shader packs assume that everything drawn afterwards is actually translucent and should cast a colored + // shadow... +// bufferSource.endBatch(); + + copyPreTranslucentDepth(); + + profiler.endStartSection("translucent terrain"); + + // TODO: Prevent these calls from scheduling translucent sorting... + // It doesn't matter a ton, since this just means that they won't be sorted in the getNormal rendering pass. + // Just something to watch out for, however... + if (shouldRenderTranslucent) { + // TODO: Render + // TODO -- This makes everything look... weird +// rg.sortAndRender(mc.thePlayer, 1, playerCamera.getPartialTicks()); + +// levelRenderer.invokeRenderChunkLayer(RenderLayer.translucent(), modelView, cameraX, cameraY, cameraZ); + } + + // Note: Apparently tripwire isn't rendered in the shadow pass. + // worldRenderer.invokeRenderType(RenderType.getTripwire(), modelView, cameraX, cameraY, cameraZ); + +// if (renderBuffersExt != null) { +// renderBuffersExt.endLevelRendering(); +// } + + // TODO: Render + debugStringTerrain = SodiumWorldRenderer.getInstance().getChunksDebugString(); + + profiler.endStartSection("generate mipmaps"); + + generateMipmaps(); + + profiler.endStartSection("restore gl state"); + + restoreGlState(); + + if (levelRenderer instanceof CullingDataCache) { + ((CullingDataCache) levelRenderer).restoreState(); + } + + // TODO: Render +// levelRenderer.setRenderBuffers(playerBuffers); + + ACTIVE = false; + profiler.endSection(); + profiler.endStartSection("updatechunks"); + } + + public void addDebugText(List messages) { + messages.add("[" + Iris.MODNAME + "] Shadow Maps: " + debugStringOverall); + messages.add("[" + Iris.MODNAME + "] Shadow Distance Terrain: " + terrainFrustumHolder.getDistanceInfo() + " Entity: " + entityFrustumHolder.getDistanceInfo()); + messages.add("[" + Iris.MODNAME + "] Shadow Culling Terrain: " + terrainFrustumHolder.getCullingInfo() + " Entity: " + entityFrustumHolder.getCullingInfo()); + messages.add("[" + Iris.MODNAME + "] Shadow Terrain: " + debugStringTerrain + (shouldRenderTerrain ? "" : " (no terrain) ") + (shouldRenderTranslucent ? "" : "(no translucent)")); + messages.add("[" + Iris.MODNAME + "] Shadow Entities: " + getEntitiesDebugString()); + messages.add("[" + Iris.MODNAME + "] Shadow Block Entities: " + getTileEntitiesDebugString()); + +// if (buffers instanceof DrawCallTrackingRenderBuffers drawCallTracker && (shouldRenderEntities || shouldRenderPlayer)) { +// messages.add("[" + Iris.MODNAME + "] Shadow Entity Batching: " + BatchingDebugMessageHelper.getDebugMessage(drawCallTracker)); +// } + } + + private String getEntitiesDebugString() { + return (shouldRenderEntities || shouldRenderPlayer) ? (renderedShadowEntities + "/" + Minecraft.getMinecraft().theWorld.loadedEntityList.size()) : "disabled by pack"; + } + + private String getTileEntitiesDebugString() { + return shouldRenderBlockEntities ? (renderedShadowTileEntities + "/" + Minecraft.getMinecraft().theWorld.loadedTileEntityList.size()) : "disabled by pack"; + } + + private static class MipmapPass { + private final int texture; + private final int targetFilteringMode; + + public MipmapPass(int texture, int targetFilteringMode) { + this.texture = texture; + this.targetFilteringMode = targetFilteringMode; + } + + public int getTexture() { + return texture; + } + + public int getTargetFilteringMode() { + return targetFilteringMode; + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/SodiumTerrainPipeline.java b/src/main/java/net/coderbot/iris/pipeline/SodiumTerrainPipeline.java new file mode 100644 index 000000000..946bf78ab --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/SodiumTerrainPipeline.java @@ -0,0 +1,180 @@ +package net.coderbot.iris.pipeline; + +import net.coderbot.iris.gl.program.ProgramImages; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.pipeline.transform.PatchShaderType; +import net.coderbot.iris.pipeline.transform.TransformPatcher; +import net.coderbot.iris.shaderpack.ProgramSet; +import net.coderbot.iris.shaderpack.ProgramSource; +import net.coderbot.iris.uniforms.CommonUniforms; +import net.coderbot.iris.uniforms.builtin.BuiltinReplacementUniforms; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.IntFunction; + +public class SodiumTerrainPipeline { + Optional terrainVertex = Optional.empty(); + Optional terrainGeometry = Optional.empty(); + Optional terrainFragment = Optional.empty(); + Optional translucentVertex = Optional.empty(); + Optional translucentGeometry = Optional.empty(); + Optional translucentFragment = Optional.empty(); + Optional shadowVertex = Optional.empty(); + Optional shadowGeometry = Optional.empty(); + Optional shadowFragment = Optional.empty(); + //GlFramebuffer framebuffer; + ProgramSet programSet; + + private final WorldRenderingPipeline parent; + + private final IntFunction createTerrainSamplers; + private final IntFunction createShadowSamplers; + + private final IntFunction createTerrainImages; + private final IntFunction createShadowImages; + + public SodiumTerrainPipeline(WorldRenderingPipeline parent, + ProgramSet programSet, IntFunction createTerrainSamplers, + IntFunction createShadowSamplers, + IntFunction createTerrainImages, + IntFunction createShadowImages) { + this.parent = Objects.requireNonNull(parent); + + Optional terrainSource = first(programSet.getGbuffersTerrain(), programSet.getGbuffersTexturedLit(), programSet.getGbuffersTextured(), programSet.getGbuffersBasic()); + Optional translucentSource = first(programSet.getGbuffersWater(), terrainSource); + Optional shadowSource = programSet.getShadow(); + + this.programSet = programSet; + + terrainSource.ifPresent(sources -> { + Map result = TransformPatcher.patchSodiumTerrain( + sources.getVertexSource().orElse(null), + sources.getGeometrySource().orElse(null), + sources.getFragmentSource().orElse(null)); + terrainVertex = Optional.ofNullable(result.get(PatchShaderType.VERTEX)); + terrainGeometry = Optional.ofNullable(result.get(PatchShaderType.GEOMETRY)); + terrainFragment = Optional.ofNullable(result.get(PatchShaderType.FRAGMENT)); + + PatchedShaderPrinter.debugPatchedShaders(sources.getName() + "_sodium", + terrainVertex.orElse(null), terrainGeometry.orElse(null), terrainFragment.orElse(null)); + }); + + translucentSource.ifPresent(sources -> { + Map result = TransformPatcher.patchSodiumTerrain( + sources.getVertexSource().orElse(null), + sources.getGeometrySource().orElse(null), + sources.getFragmentSource().orElse(null)); + translucentVertex = Optional.ofNullable(result.get(PatchShaderType.VERTEX)); + translucentGeometry = Optional.ofNullable(result.get(PatchShaderType.GEOMETRY)); + translucentFragment = Optional.ofNullable(result.get(PatchShaderType.FRAGMENT)); + + PatchedShaderPrinter.debugPatchedShaders(sources.getName() + "_sodium", + translucentVertex.orElse(null), translucentGeometry.orElse(null), translucentFragment.orElse(null)); + }); + + shadowSource.ifPresent(sources -> { + Map result = TransformPatcher.patchSodiumTerrain( + sources.getVertexSource().orElse(null), + sources.getGeometrySource().orElse(null), + sources.getFragmentSource().orElse(null)); + shadowVertex = Optional.ofNullable(result.get(PatchShaderType.VERTEX)); + shadowGeometry = Optional.ofNullable(result.get(PatchShaderType.GEOMETRY)); + shadowFragment = Optional.ofNullable(result.get(PatchShaderType.FRAGMENT)); + + PatchedShaderPrinter.debugPatchedShaders(sources.getName() + "_sodium", + shadowVertex.orElse(null), shadowGeometry.orElse(null), shadowFragment.orElse(null)); + }); + + this.createTerrainSamplers = createTerrainSamplers; + this.createShadowSamplers = createShadowSamplers; + this.createTerrainImages = createTerrainImages; + this.createShadowImages = createShadowImages; + } + + public Optional getTerrainVertexShaderSource() { + return terrainVertex; + } + + public Optional getTerrainGeometryShaderSource() { + return terrainGeometry; + } + + public Optional getTerrainFragmentShaderSource() { + return terrainFragment; + } + + public Optional getTranslucentVertexShaderSource() { + return translucentVertex; + } + + public Optional getTranslucentGeometryShaderSource() { + return translucentGeometry; + } + + public Optional getTranslucentFragmentShaderSource() { + return translucentFragment; + } + + public Optional getShadowVertexShaderSource() { + return shadowVertex; + } + + public Optional getShadowGeometryShaderSource() { + return shadowGeometry; + } + + public Optional getShadowFragmentShaderSource() { + return shadowFragment; + } + + public ProgramUniforms initUniforms(int programId) { + ProgramUniforms.Builder uniforms = ProgramUniforms.builder("", programId); + + CommonUniforms.addCommonUniforms(uniforms, programSet.getPack().getIdMap(), programSet.getPackDirectives(), parent.getFrameUpdateNotifier()); + BuiltinReplacementUniforms.addBuiltinReplacementUniforms(uniforms); + + return uniforms.buildUniforms(); + } + + public boolean hasShadowPass() { + return createShadowSamplers != null; + } + + public ProgramSamplers initTerrainSamplers(int programId) { + return createTerrainSamplers.apply(programId); + } + + public ProgramSamplers initShadowSamplers(int programId) { + return createShadowSamplers.apply(programId); + } + + public ProgramImages initTerrainImages(int programId) { + return createTerrainImages.apply(programId); + } + + public ProgramImages initShadowImages(int programId) { + return createShadowImages.apply(programId); + } + + /*public void bindFramebuffer() { + this.framebuffer.bind(); + } + + public void unbindFramebuffer() { + GlStateManager.bindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + }*/ + + @SafeVarargs + private static Optional first(Optional... candidates) { + for (Optional candidate : candidates) { + if (candidate.isPresent()) { + return candidate; + } + } + + return Optional.empty(); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPhase.java b/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPhase.java new file mode 100644 index 000000000..81a1e2224 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPhase.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; + +public enum WorldRenderingPhase { + NONE, + SKY, + SUNSET, + CUSTOM_SKY, // Unused, just here to match OptiFine ordinals + SUN, + MOON, + STARS, + VOID, + TERRAIN_SOLID, + TERRAIN_CUTOUT_MIPPED, + TERRAIN_CUTOUT, + ENTITIES, + BLOCK_ENTITIES, + DESTROY, + OUTLINE, + DEBUG, + HAND_SOLID, + TERRAIN_TRANSLUCENT, + TRIPWIRE, + PARTICLES, + CLOUDS, + RAIN_SNOW, + WORLD_BORDER, + HAND_TRANSLUCENT; + +} diff --git a/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPipeline.java new file mode 100644 index 000000000..4dc2904f0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/WorldRenderingPipeline.java @@ -0,0 +1,57 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.compat.mojang.Camera; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; +import net.coderbot.iris.gbuffer_overrides.state.RenderTargetStateListener; +import net.coderbot.iris.shaderpack.CloudSetting; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.minecraft.client.renderer.EntityRenderer; + +import java.util.List; +import java.util.OptionalInt; + +public interface WorldRenderingPipeline { + void beginLevelRendering(); + void renderShadows(EntityRenderer levelRenderer, Camera camera); + void addDebugText(List messages); + OptionalInt getForcedShadowRenderDistanceChunksForDisplay(); + + WorldRenderingPhase getPhase(); + + void beginSodiumTerrainRendering(); + void endSodiumTerrainRendering(); + void setOverridePhase(WorldRenderingPhase phase); + void setPhase(WorldRenderingPhase phase); + void setInputs(InputAvailability availability); + void setSpecialCondition(SpecialCondition special); + void syncProgram(); + RenderTargetStateListener getRenderTargetStateListener(); + + int getCurrentNormalTexture(); + int getCurrentSpecularTexture(); + + void onBindTexture(int id); + + void beginHand(); + + void beginTranslucents(); + void finalizeLevelRendering(); + void destroy(); + + SodiumTerrainPipeline getSodiumTerrainPipeline(); + FrameUpdateNotifier getFrameUpdateNotifier(); + + boolean shouldDisableVanillaEntityShadows(); + boolean shouldDisableDirectionalShading(); + CloudSetting getCloudSetting(); + boolean shouldRenderUnderwaterOverlay(); + boolean shouldRenderVignette(); + boolean shouldRenderSun(); + boolean shouldRenderMoon(); + boolean shouldWriteRainAndSnowToDepthBuffer(); + boolean shouldRenderParticlesBeforeDeferred(); + boolean allowConcurrentCompute(); + + float getSunPathRotation(); +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/AttributeParameters.java b/src/main/java/net/coderbot/iris/pipeline/transform/AttributeParameters.java new file mode 100644 index 000000000..11d85b53f --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/AttributeParameters.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.pipeline.transform; + +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; + +class AttributeParameters extends Parameters { + public final boolean hasGeometry; + public final InputAvailability inputs; + + public AttributeParameters(Patch patch, boolean hasGeometry, InputAvailability inputs) { + super(patch); + this.hasGeometry = hasGeometry; + this.inputs = inputs; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (hasGeometry ? 1231 : 1237); + result = prime * result + ((inputs == null) ? 0 : inputs.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + AttributeParameters other = (AttributeParameters) obj; + if (hasGeometry != other.hasGeometry) + return false; + if (inputs == null) { + if (other.inputs != null) + return false; + } else if (!inputs.equals(other.inputs)) + return false; + return true; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/AttributeTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/AttributeTransformer.java new file mode 100644 index 000000000..bf0c333cd --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/AttributeTransformer.java @@ -0,0 +1,167 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.Identifier; +import io.github.douira.glsl_transformer.ast.node.Profile; +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.basic.ASTNode; +import io.github.douira.glsl_transformer.ast.node.external_declaration.ExternalDeclaration; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.query.match.AutoHintedMatcher; +import io.github.douira.glsl_transformer.ast.query.match.Matcher; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; +import net.coderbot.iris.gl.shader.ShaderType; + +import java.util.stream.Stream; + +/** + * Implements AttributeShaderTransformer using glsl-transformer AST + * transformation methods. + */ +class AttributeTransformer { + public static void transform( + ASTParser t, + TranslationUnit tree, + Root root, + AttributeParameters parameters) { + boolean isCore = (tree.getVersionStatement().profile == Profile.CORE || (tree.getVersionStatement().profile == null && tree.getVersionStatement().version.number > 140)); + if (isCore) { + if (parameters.type == PatchShaderType.VERTEX) { + throw new IllegalStateException("Vertex shaders must be in the compatibility profile to run properly!"); + } + return; + } + + // gl_MultiTexCoord1 and gl_MultiTexCoord2 are both ways to refer to the + // lightmap texture coordinate. + // See https://github.com/IrisShaders/Iris/issues/1149 + if (parameters.inputs.lightmap) { + root.rename("gl_MultiTexCoord1", "gl_MultiTexCoord2"); + } + + Stream stream = Stream.empty(); + boolean hasItems = false; + if (!parameters.inputs.lightmap) { + stream = Stream.concat(stream, + root.identifierIndex.getStream("gl_MultiTexCoord1")); + stream = Stream.concat(stream, + root.identifierIndex.getStream("gl_MultiTexCoord2")); + hasItems = true; + } + if (!parameters.inputs.texture) { + stream = Stream.concat(stream, + root.identifierIndex.getStream("gl_MultiTexCoord0")); + hasItems = true; + } + if (hasItems) { + root.replaceReferenceExpressions(t, stream, "vec4(240.0, 240.0, 0.0, 1.0)"); + } + + patchTextureMatrices(t, tree, root, parameters.inputs.lightmap); + + if (parameters.inputs.overlay) { + patchOverlayColor(t, tree, root, parameters); + } + + if (parameters.type.glShaderType == ShaderType.VERTEX + && root.identifierIndex.has("gl_MultiTexCoord3") + && !root.identifierIndex.has("mc_midTexCoord")) { + // TODO: proper type conversion + // gl_MultiTexCoord3 is a super legacy alias of mc_midTexCoord. We don't do this + // replacement if we think mc_midTexCoord could be defined just we can't handle + // an existing declaration robustly. But basically the proper way to do this is + // to define mc_midTexCoord only if it's not defined, and if it is defined, + // figure out its type, then replace all occurrences of gl_MultiTexCoord3 with + // the correct conversion from mc_midTexCoord's declared type to vec4. + root.rename("gl_MultiTexCoord3", "mc_midTexCoord"); + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "attribute vec4 mc_midTexCoord;"); + } + } + + private static void patchTextureMatrices( + ASTParser t, + TranslationUnit tree, + Root root, + boolean hasLightmap) { + root.rename("gl_TextureMatrix", "iris_TextureMatrix"); + + tree.parseAndInjectNodes(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "const float iris_ONE_OVER_256 = 0.00390625;", + "const float iris_ONE_OVER_32 = iris_ONE_OVER_256 * 8;"); + if (hasLightmap) { + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "mat4 iris_LightmapTextureMatrix = gl_TextureMatrix[2];"); + } else { + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, "mat4 iris_LightmapTextureMatrix =" + + "mat4(iris_ONE_OVER_256, 0.0, 0.0, 0.0," + + " 0.0, iris_ONE_OVER_256, 0.0, 0.0," + + " 0.0, 0.0, iris_ONE_OVER_256, 0.0," + + " iris_ONE_OVER_32, iris_ONE_OVER_32, iris_ONE_OVER_32, iris_ONE_OVER_256);"); + } + + // column major + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, "mat4 iris_TextureMatrix[8] = mat4[8](" + + "gl_TextureMatrix[0]," + + "iris_LightmapTextureMatrix," + + "mat4(1.0)," + + "mat4(1.0)," + + "mat4(1.0)," + + "mat4(1.0)," + + "mat4(1.0)," + + "mat4(1.0)" + + ");"); + } + + private static final AutoHintedMatcher uniformVec4EntityColor = new AutoHintedMatcher<>( + "uniform vec4 entityColor;", Matcher.externalDeclarationPattern); + + // Add entity color -> overlay color attribute support. + private static void patchOverlayColor( + ASTParser t, + TranslationUnit tree, + Root root, + AttributeParameters parameters) { + // delete original declaration + root.processMatches(t, uniformVec4EntityColor, ASTNode::detachAndDelete); + + if (parameters.type.glShaderType == ShaderType.VERTEX) { + // add our own declarations + // TODO: We're exposing entityColor to this stage even if it isn't declared in + // this stage. But this is needed for the pass-through behavior. + tree.parseAndInjectNodes(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "uniform sampler2D iris_overlay;", + "varying vec4 entityColor;"); + + // this is so we can pass through the overlay color at the end to the geometry + // or fragment stage. + tree.prependMain(t, + "vec4 overlayColor = texture2D(iris_overlay, (gl_TextureMatrix[1] * gl_MultiTexCoord1).xy);", + "entityColor = vec4(overlayColor.rgb, 1.0 - overlayColor.a);", + // Workaround for a shader pack bug: + // https://github.com/IrisShaders/Iris/issues/1549 + // Some shader packs incorrectly ignore the alpha value, and assume that rgb + // will be + // zero if there is no hit flash, we try to emulate that here + "entityColor.rgb *= float(entityColor.a != 0.0);"); + } else if (parameters.type.glShaderType == ShaderType.GEOMETRY) { + // replace read references to grab the color from the first vertex. + root.replaceReferenceExpressions(t, "entityColor", "entityColor[0]"); + + // TODO: this is passthrough behavior + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "out vec4 entityColorGS;"); + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_FUNCTIONS, + "in vec4 entityColor[];"); + tree.prependMain(t, "entityColorGS = entityColor[0];"); + } else if (parameters.type.glShaderType == ShaderType.FRAGMENT) { + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_DECLARATIONS, + "varying vec4 entityColor;"); + + // Different output name to avoid a name collision in the geometry shader. + if (parameters.hasGeometry) { + root.rename("entityColor", "entityColorGS"); + } + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/CompatibilityTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/CompatibilityTransformer.java new file mode 100644 index 000000000..cb31757e1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/CompatibilityTransformer.java @@ -0,0 +1,544 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.Identifier; +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.basic.ASTNode; +import io.github.douira.glsl_transformer.ast.node.declaration.DeclarationMember; +import io.github.douira.glsl_transformer.ast.node.declaration.FunctionParameter; +import io.github.douira.glsl_transformer.ast.node.declaration.TypeAndInitDeclaration; +import io.github.douira.glsl_transformer.ast.node.expression.Expression; +import io.github.douira.glsl_transformer.ast.node.expression.LiteralExpression; +import io.github.douira.glsl_transformer.ast.node.expression.ReferenceExpression; +import io.github.douira.glsl_transformer.ast.node.expression.unary.FunctionCallExpression; +import io.github.douira.glsl_transformer.ast.node.external_declaration.DeclarationExternalDeclaration; +import io.github.douira.glsl_transformer.ast.node.external_declaration.EmptyDeclaration; +import io.github.douira.glsl_transformer.ast.node.external_declaration.ExternalDeclaration; +import io.github.douira.glsl_transformer.ast.node.external_declaration.FunctionDefinition; +import io.github.douira.glsl_transformer.ast.node.statement.Statement; +import io.github.douira.glsl_transformer.ast.node.type.qualifier.StorageQualifier; +import io.github.douira.glsl_transformer.ast.node.type.qualifier.StorageQualifier.StorageType; +import io.github.douira.glsl_transformer.ast.node.type.qualifier.TypeQualifier; +import io.github.douira.glsl_transformer.ast.node.type.qualifier.TypeQualifierPart; +import io.github.douira.glsl_transformer.ast.node.type.specifier.BuiltinNumericTypeSpecifier; +import io.github.douira.glsl_transformer.ast.node.type.specifier.FunctionPrototype; +import io.github.douira.glsl_transformer.ast.node.type.specifier.TypeSpecifier; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.query.match.AutoHintedMatcher; +import io.github.douira.glsl_transformer.ast.query.match.Matcher; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; +import io.github.douira.glsl_transformer.ast.transform.Template; +import io.github.douira.glsl_transformer.util.Type; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.shader.ShaderType; +import net.coderbot.iris.pipeline.PatchedShaderPrinter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class CompatibilityTransformer { + static Logger LOGGER = LogManager.getLogger(CompatibilityTransformer.class); + + private static final AutoHintedMatcher sildursWaterFract = new AutoHintedMatcher<>( + "fract(worldpos.y + 0.001)", Matcher.expressionPattern); + + private static StorageQualifier getConstQualifier(TypeQualifier qualifier) { + if (qualifier == null) { + return null; + } + for (TypeQualifierPart constQualifier : qualifier.getChildren()) { + if (constQualifier instanceof StorageQualifier) { + StorageQualifier storageQualifier = (StorageQualifier) constQualifier; + if (storageQualifier.storageType == StorageQualifier.StorageType.CONST) { + return storageQualifier; + } + } + } + return null; + } + + public static void transformEach(ASTParser t, TranslationUnit tree, Root root, Parameters parameters) { + if (parameters.type == PatchShaderType.VERTEX) { + if (root.replaceExpressionMatches(t, sildursWaterFract, "fract(worldpos.y + 0.01)")) { + Iris.logger.warn("Patched fract(worldpos.y + 0.001) to fract(worldpos.y + 0.01) to fix " + + "waving water disconnecting from other water blocks; See https://github.com/IrisShaders/Iris/issues/509"); + } + } + + /** + * Removes const storage qualifier from declarations in functions if they are + * initialized with const parameters. Const parameters are immutable parameters + * and can't be used to initialize const declarations because they expect + * constant, not just immutable, expressions. This varies between drivers and + * versions. Also removes the const qualifier from declarations that use the + * identifiers from which the declaration was removed previously. + * See https://wiki.shaderlabs.org/wiki/Compiler_Behavior_Notes + */ + Map> constFunctions = new HashMap<>(); + Set processingSet = new HashSet<>(); + List unusedFunctions = new LinkedList<>(); + for (FunctionDefinition definition : root.nodeIndex.get(FunctionDefinition.class)) { + // check if this function is ever used + FunctionPrototype prototype = definition.getFunctionPrototype(); + String functionName = prototype.getName().getName(); + if (!functionName.equals("main") && root.identifierIndex.getStream(functionName).count() <= 1) { + // remove unused functions + // unused function removal can be helpful since some drivers don't do some + // checks on unused functions. Additionally, sometimes bugs in unused code can + // be avoided this way. + // TODO: integrate into debug mode (allow user to disable this behavior for + // debugging purposes) + unusedFunctions.add(definition); + if (PatchedShaderPrinter.prettyPrintShaders) { + LOGGER.warn("Removing unused function " + functionName); + } else if (unusedFunctions.size() == 1) { + LOGGER.warn( + "Removing unused function " + functionName + + " and omitting further such messages outside of debug mode. See debugging.md for more information."); + } + continue; + } + + // stop on functions without parameters + if (prototype.getChildren().isEmpty()) { + continue; + } + + // find the const parameters + Set names = new HashSet<>(prototype.getChildren().size()); + for (FunctionParameter parameter : prototype.getChildren()) { + if (getConstQualifier(parameter.getType().getTypeQualifier()) != null) { + String name = parameter.getName().getName(); + names.add(name); + processingSet.add(name); + } + } + if (!names.isEmpty()) { + constFunctions.put(definition, names); + } + } + + // remove collected unused functions + for (FunctionDefinition definition : unusedFunctions) { + definition.detachAndDelete(); + } + + // find the reference expressions for the const parameters + // and check that they are in the right function and are of the right type + boolean constDeclarationHit = false; + Deque processingQueue = new ArrayDeque<>(processingSet); + while (!processingQueue.isEmpty()) { + String name = processingQueue.poll(); + processingSet.remove(name); + for (Identifier id : root.identifierIndex.get(name)) { + // since this searches for reference expressions, this won't accidentally find + // the name as the name of a declaration member + ReferenceExpression reference = id.getAncestor(ReferenceExpression.class); + if (reference == null) { + continue; + } + TypeAndInitDeclaration taid = reference.getAncestor(TypeAndInitDeclaration.class); + if (taid == null) { + continue; + } + FunctionDefinition inDefinition = taid.getAncestor(FunctionDefinition.class); + if (inDefinition == null) { + continue; + } + Set constIdsInFunction = constFunctions.get(inDefinition); + if (constIdsInFunction == null) { + continue; + } + if (constIdsInFunction.contains(name)) { + // remove the const qualifier from the reference expression + TypeQualifier qualifier = taid.getType().getTypeQualifier(); + StorageQualifier constQualifier = getConstQualifier(qualifier); + if (constQualifier == null) { + continue; + } + constQualifier.detachAndDelete(); + if (qualifier.getChildren().isEmpty()) { + qualifier.detachAndDelete(); + } + constDeclarationHit = true; + + // add all members of the declaration to the list of const parameters to process + for (DeclarationMember member : taid.getMembers()) { + String memberName = member.getName().getName(); + + // the name may not be the same as the parameter name + if (constIdsInFunction.contains(memberName)) { + throw new IllegalStateException("Illegal redefinition of const parameter " + name); + } + + constIdsInFunction.add(memberName); + + // don't add to the queue twice if it's already been added by a different scope + if (!processingSet.contains(memberName)) { + processingQueue.add(memberName); + processingSet.add(memberName); + } + } + } + } + } + + if (constDeclarationHit) { + LOGGER.warn( + "Removed the const keyword from declarations that use const parameters. See debugging.md for more information."); + } + + // remove empty external declarations + boolean emptyDeclarationHit = root.process( + root.nodeIndex.getStream(EmptyDeclaration.class), + ASTNode::detachAndDelete); + if (emptyDeclarationHit) { + LOGGER.warn( + "Removed empty external declarations (\";\")."); + } + } + + private static class DeclarationMatcher extends AutoHintedMatcher { + private final StorageType storageType; + + public DeclarationMatcher(StorageType storageType) { + super("out float name;", Matcher.externalDeclarationPattern); + this.storageType = storageType; + markClassWildcard("qualifier", pattern.getRoot().nodeIndex.getOne(TypeQualifier.class)); + markClassWildcard("type", pattern.getRoot().nodeIndex.getOne(BuiltinNumericTypeSpecifier.class)); + markClassWildcard("name*", pattern.getRoot().identifierIndex.getOne("name").getAncestor(DeclarationMember.class)); + } + + public boolean matchesSpecialized(ExternalDeclaration tree, ShaderType shaderType) { + boolean result = super.matchesExtract(tree); + if (!result) { + return false; + } + TypeQualifier qualifier = getNodeMatch("qualifier", TypeQualifier.class); + for (TypeQualifierPart part : qualifier.getParts()) { + if (part instanceof StorageQualifier) { + StorageQualifier storageQualifier = (StorageQualifier) part; + StorageType qualifierStorageType = storageQualifier.storageType; + // TODO: investigate the specified behavior of varying in geometry shaders and + // the combination of varying with in or out qualifiers + if (qualifierStorageType == storageType || + qualifierStorageType == StorageType.VARYING && + (shaderType == ShaderType.VERTEX && storageType == StorageType.OUT || + shaderType == ShaderType.GEOMETRY && storageType == StorageType.OUT || + shaderType == ShaderType.FRAGMENT && storageType == StorageType.IN)) { + return true; + } + } + } + return false; + } + } + + private static final ShaderType[] pipeline = { ShaderType.VERTEX, ShaderType.GEOMETRY, ShaderType.FRAGMENT }; + private static final DeclarationMatcher outDeclarationMatcher = new DeclarationMatcher(StorageType.OUT); + private static final DeclarationMatcher inDeclarationMatcher = new DeclarationMatcher(StorageType.IN); + + private static final String tagPrefix = "iris_template_"; + private static final Template declarationTemplate = Template + .withExternalDeclaration("out __type __name;"); + private static final Template initTemplate = Template.withStatement("__decl = __value;"); + private static final Template variableTemplate = Template + .withExternalDeclaration("__type __internalDecl;"); + private static final Template statementTemplate = Template + .withStatement("__oldDecl = vec3(__internalDecl);"); + private static final Template statementTemplateVector = Template + .withStatement("__oldDecl = vec3(__internalDecl, vec4(0));"); + + static { + declarationTemplate.markLocalReplacement(declarationTemplate.getSourceRoot().nodeIndex.getOne(TypeQualifier.class)); + declarationTemplate.markLocalReplacement("__type", TypeSpecifier.class); + declarationTemplate.markIdentifierReplacement("__name"); + initTemplate.markIdentifierReplacement("__decl"); + initTemplate.markLocalReplacement("__value", ReferenceExpression.class); + variableTemplate.markLocalReplacement("__type", TypeSpecifier.class); + variableTemplate.markIdentifierReplacement("__internalDecl"); + statementTemplate.markIdentifierReplacement("__oldDecl"); + statementTemplate.markIdentifierReplacement("__internalDecl"); + statementTemplate.markLocalReplacement( + statementTemplate.getSourceRoot().nodeIndex.getStream(BuiltinNumericTypeSpecifier.class) + .filter(specifier -> specifier.type == Type.F32VEC3).findAny().get()); + statementTemplateVector.markIdentifierReplacement("__oldDecl"); + statementTemplateVector.markIdentifierReplacement("__internalDecl"); + statementTemplateVector.markLocalReplacement( + statementTemplateVector.getSourceRoot().nodeIndex.getStream(BuiltinNumericTypeSpecifier.class) + .filter(specifier -> specifier.type == Type.F32VEC3).findAny().get()); + } + + private static Statement getInitializer(Root root, String name, Type type) { + return initTemplate.getInstanceFor(root, + new Identifier(name), + type.isScalar() + ? LiteralExpression.getDefaultValue(type) + : Root.indexNodes(root, () -> new FunctionCallExpression( + new Identifier(type.getMostCompactName()), + Stream.of(LiteralExpression.getDefaultValue(type))))); + } + + private static TypeQualifier makeQualifierOut(TypeQualifier typeQualifier) { + for (TypeQualifierPart qualifierPart : typeQualifier.getParts()) { + if (qualifierPart instanceof StorageQualifier) { + StorageQualifier storageQualifier = (StorageQualifier) qualifierPart; + if (((StorageQualifier) qualifierPart).storageType == StorageType.IN) { + storageQualifier.storageType = StorageType.OUT; + } + } + } + return typeQualifier; + } + + // does transformations that require cross-shader type data + public static void transformGrouped( + ASTParser t, + Map trees, + Parameters parameters) { + /** + * find attributes that are declared as "in" in geometry or fragment but not + * declared as "out" in the previous stage. The missing "out" declarations for + * these attributes are added and initialized. + * + * It doesn't bother with array specifiers because they are only legal in + * geometry shaders, but then also only as an in declaration. The out + * declaration in the vertex shader is still just a single value. Missing out + * declarations in the geometry shader are also just normal. + * + * TODO: + * - fix issues where Iris' own declarations are detected and patched like + * iris_FogFragCoord if there are geometry shaders present + * - improved geometry shader support? They use funky declarations + */ + ShaderType prevType = null; + for (int i = 0; i < pipeline.length; i++) { + ShaderType type = pipeline[i]; + PatchShaderType[] patchTypes = PatchShaderType.fromGlShaderType(type); + + // check if the patch types have sources and continue if not + boolean hasAny = false; + for (PatchShaderType currentType : patchTypes) { + if (trees.get(currentType) != null) { + hasAny = true; + } + } + if (!hasAny) { + continue; + } + + // if the current type has sources but the previous one doesn't, set the + // previous one and continue + if (prevType == null) { + prevType = type; + continue; + } + + PatchShaderType prevPatchTypes = PatchShaderType.fromGlShaderType(prevType)[0]; + TranslationUnit prevTree = trees.get(prevPatchTypes); + Root prevRoot = prevTree.getRoot(); + + // test if the prefix tag is used for some reason + if (prevRoot.identifierIndex.prefixQueryFlat(tagPrefix).findAny().isPresent()) { + LOGGER.warn("The prefix tag " + tagPrefix + " is used in the shader, bailing compatibility transformation."); + return; + } + + // find out declarations + Map outDeclarations = new HashMap<>(); + for (DeclarationExternalDeclaration declaration : prevRoot.nodeIndex.get(DeclarationExternalDeclaration.class)) { + if (outDeclarationMatcher.matchesSpecialized(declaration, prevType)) { + BuiltinNumericTypeSpecifier extractedType = outDeclarationMatcher.getNodeMatch("type", + BuiltinNumericTypeSpecifier.class); + for (DeclarationMember member : outDeclarationMatcher + .getNodeMatch("name*", DeclarationMember.class) + .getAncestor(TypeAndInitDeclaration.class) + .getMembers()) { + String name = member.getName().getName(); + if (!name.startsWith("gl_")) { + outDeclarations.put(name, extractedType); + } + } + } + } + + // add out declarations that are missing for in declarations + for (PatchShaderType currentType : patchTypes) { + TranslationUnit currentTree = trees.get(currentType); + if (currentTree == null) { + continue; + } + Root currentRoot = currentTree.getRoot(); + + for (ExternalDeclaration declaration : currentRoot.nodeIndex.get(DeclarationExternalDeclaration.class)) { + if (!inDeclarationMatcher.matchesSpecialized(declaration, currentType.glShaderType)) { + continue; + } + + BuiltinNumericTypeSpecifier inTypeSpecifier = inDeclarationMatcher.getNodeMatch("type", + BuiltinNumericTypeSpecifier.class); + for (DeclarationMember inDeclarationMember : inDeclarationMatcher + .getNodeMatch("name*", DeclarationMember.class) + .getAncestor(TypeAndInitDeclaration.class) + .getMembers()) { + String name = inDeclarationMember.getName().getName(); + if (name.startsWith("gl_")) { + continue; + } + + // patch missing declarations with an initialization + if (!outDeclarations.containsKey(name)) { + // make sure the declared in is actually used + if (!currentRoot.identifierIndex.getAncestors(name, ReferenceExpression.class).findAny().isPresent()) { + continue; + } + + if (inTypeSpecifier == null) { + LOGGER.warn( + "The in declaration '" + name + "' in the " + currentType.glShaderType.name() + + " shader that has a missing corresponding out declaration in the previous stage " + + prevType.name() + " has a non-numeric type and could not be compatibility-patched. See debugging.md for more information."); + continue; + } + Type inType = inTypeSpecifier.type; + + // insert the new out declaration but copy over the type qualifiers, except for + // the in/out qualifier + TypeQualifier outQualifier = (TypeQualifier) inDeclarationMatcher + .getNodeMatch("qualifier").cloneInto(prevRoot); + makeQualifierOut(outQualifier); + prevTree.injectNode(ASTInjectionPoint.BEFORE_DECLARATIONS, declarationTemplate.getInstanceFor(prevRoot, + outQualifier, + inTypeSpecifier.cloneInto(prevRoot), + new Identifier(name))); + + // add the initializer to the main function + prevTree.prependMain(getInitializer(prevRoot, name, inType)); + + // update out declarations to prevent duplicates + outDeclarations.put(name, null); + + LOGGER.warn( + "The in declaration '" + name + "' in the " + currentType.glShaderType.name() + + " shader is missing a corresponding out declaration in the previous stage " + + prevType.name() + " and has been compatibility-patched. See debugging.md for more information."); + } + + // patch mismatching declaration with a local variable and a cast + else { + // there is an out declaration for this in declaration, check if the types match + BuiltinNumericTypeSpecifier outTypeSpecifier = outDeclarations.get(name); + + // skip newly inserted out declarations + if (outTypeSpecifier == null) { + continue; + } + + Type inType = inTypeSpecifier.type; + Type outType = outTypeSpecifier.type; + + // skip if the type matches, nothing has to be done + if (inType == outType) { + // if the types match but it's never assigned a value, + // an initialization is added + if (prevRoot.identifierIndex.get(name).size() > 1) { + continue; + } + + // add an initialization statement for this declaration + prevTree.prependMain(getInitializer(prevRoot, name, inType)); + outDeclarations.put(name, null); + continue; + } + + // bail and warn on mismatching dimensionality + if (outType.getDimension() != inType.getDimension()) { + LOGGER.warn( + "The in declaration '" + name + "' in the " + currentType.glShaderType.name() + + " shader has a mismatching dimensionality (scalar/vector/matrix) with the out declaration in the previous stage " + + prevType.name() + " and could not be compatibility-patched. See debugging.md for more information."); + continue; + } + + boolean isVector = outType.isVector(); + + // rename all references of this out declaration to a new name (iris_) + String newName = tagPrefix + name; + prevRoot.identifierIndex.rename(name, newName); + + // rename the original out declaration back to the original name + TypeAndInitDeclaration outDeclaration = outTypeSpecifier.getAncestor(TypeAndInitDeclaration.class); + if (outDeclaration == null) { + continue; + } + + List outMembers = outDeclaration.getMembers(); + DeclarationMember outMember = null; + for (DeclarationMember member : outMembers) { + if (member.getName().getName().equals(newName)) { + outMember = member; + } + } + if (outMember == null) { + throw new IllegalStateException("The targeted out declaration member is missing!"); + } + outMember.getName().replaceByAndDelete(new Identifier(name)); + + // move the declaration member out of the declaration in case there is more than + // one member to avoid changing the other member's type as well. + if (outMembers.size() > 1) { + outMember.detach(); + outTypeSpecifier = outTypeSpecifier.cloneInto(prevRoot); + DeclarationExternalDeclaration singleOutDeclaration = (DeclarationExternalDeclaration) declarationTemplate + .getInstanceFor(prevRoot, + makeQualifierOut(outDeclaration.getType().getTypeQualifier().cloneInto(prevRoot)), + outTypeSpecifier, + new Identifier(name)); + ((TypeAndInitDeclaration) singleOutDeclaration.getDeclaration()).getMembers().set(0, outMember); + prevTree.injectNode(ASTInjectionPoint.BEFORE_DECLARATIONS, singleOutDeclaration); + } + + // add a global variable with the new name and the old type + prevTree.injectNode(ASTInjectionPoint.BEFORE_DECLARATIONS, variableTemplate.getInstanceFor(prevRoot, + outTypeSpecifier.cloneInto(prevRoot), + new Identifier(newName))); + + // insert a statement at the end of the main function that sets the value of the + // out declaration to the value of the global variable and does a type cast + prevTree.appendMain( + (isVector && outType.getDimensions()[0] < inType.getDimensions()[0] ? statementTemplateVector + : statementTemplate).getInstanceFor(prevRoot, + new Identifier(name), + new Identifier(newName), + inTypeSpecifier.cloneInto(prevRoot))); + + // make the out declaration use the same type as the fragment shader + outTypeSpecifier.replaceByAndDelete(inTypeSpecifier.cloneInto(prevRoot)); + + // don't do the patch twice + outDeclarations.put(name, null); + + LOGGER.warn( + "The out declaration '" + name + "' in the " + prevType.name() + + " shader has a different type " + outType.getMostCompactName() + + " than the corresponding in declaration of type " + inType.getMostCompactName() + + " in the following stage " + currentType.glShaderType.name() + + " and has been compatibility-patched. See debugging.md for more information."); + } + } + } + } + + prevType = type; + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/CompositeDepthTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/CompositeDepthTransformer.java new file mode 100644 index 000000000..105a6ba35 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/CompositeDepthTransformer.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.basic.ASTNode; +import io.github.douira.glsl_transformer.ast.node.external_declaration.ExternalDeclaration; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.query.match.AutoHintedMatcher; +import io.github.douira.glsl_transformer.ast.query.match.Matcher; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; + +class CompositeDepthTransformer { + private static final AutoHintedMatcher uniformFloatCenterDepthSmooth = new AutoHintedMatcher<>( + "uniform float centerDepthSmooth;", Matcher.externalDeclarationPattern); + + public static void transform( + ASTParser t, + TranslationUnit tree, + Root root) { + // replace original declaration + if (root.processMatches(t, uniformFloatCenterDepthSmooth, ASTNode::detachAndDelete)) { + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_DECLARATIONS, + "uniform sampler2D iris_centerDepthSmooth;"); + + // if centerDepthSmooth is not declared as a uniform, we don't make it available + root.replaceReferenceExpressions(t, "centerDepthSmooth", + "texture2D(iris_centerDepthSmooth, vec2(0.5)).r"); + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/CompositeTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/CompositeTransformer.java new file mode 100644 index 000000000..e058d6c3a --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/CompositeTransformer.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.expression.unary.FunctionCallExpression; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; + +import java.util.stream.Stream; + +class CompositeTransformer { + public static void transform( + ASTParser t, + TranslationUnit tree, + Root root) { + CompositeDepthTransformer.transform(t, tree, root); + + // if using a lod texture sampler and on version 120, patch in the extension + // #extension GL_ARB_shader_texture_lod : require + if (tree.getVersionStatement().version.number <= 120 + && Stream.concat( + root.identifierIndex.getStream("texture2DLod"), + root.identifierIndex.getStream("texture3DLod")) + .filter(id -> id.getParent() instanceof FunctionCallExpression) + .findAny().isPresent()) { + tree.parseAndInjectNode(t, ASTInjectionPoint.BEFORE_DECLARATIONS, + "#extension GL_ARB_shader_texture_lod : require\n"); + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/Parameters.java b/src/main/java/net/coderbot/iris/pipeline/transform/Parameters.java new file mode 100644 index 000000000..47d5b2521 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/Parameters.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.job_parameter.JobParameters; + +class Parameters extends JobParameters { + public final Patch patch; + public PatchShaderType type; + + public Parameters(Patch patch) { + this.patch = patch; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((patch == null) ? 0 : patch.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Parameters other = (Parameters) obj; + if (patch != other.patch) + return false; + if (type != other.type) + return false; + return true; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/Patch.java b/src/main/java/net/coderbot/iris/pipeline/transform/Patch.java new file mode 100644 index 000000000..83332bba5 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/Patch.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.pipeline.transform; + +enum Patch { + ATTRIBUTES, + SODIUM_TERRAIN, + COMPOSITE +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/PatchShaderType.java b/src/main/java/net/coderbot/iris/pipeline/transform/PatchShaderType.java new file mode 100644 index 000000000..d11797dba --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/PatchShaderType.java @@ -0,0 +1,24 @@ +package net.coderbot.iris.pipeline.transform; + +import net.coderbot.iris.gl.shader.ShaderType; + +public enum PatchShaderType { + VERTEX(ShaderType.VERTEX), + GEOMETRY(ShaderType.GEOMETRY), + FRAGMENT(ShaderType.FRAGMENT); + + public final ShaderType glShaderType; + + private PatchShaderType(ShaderType glShaderType) { + this.glShaderType = glShaderType; + } + + public static PatchShaderType[] fromGlShaderType(ShaderType glShaderType) { + return switch (glShaderType) { + case VERTEX -> new PatchShaderType[] { VERTEX }; + case GEOMETRY -> new PatchShaderType[] { GEOMETRY }; + case FRAGMENT -> new PatchShaderType[] { FRAGMENT }; + default -> throw new IllegalArgumentException("Unknown shader type: " + glShaderType); + }; + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/SodiumTerrainTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/SodiumTerrainTransformer.java new file mode 100644 index 000000000..81473b844 --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/SodiumTerrainTransformer.java @@ -0,0 +1,112 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.expression.Expression; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.query.match.AutoHintedMatcher; +import io.github.douira.glsl_transformer.ast.query.match.Matcher; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; + +/** + * Does the sodium terrain transformations using glsl-transformer AST. + */ +class SodiumTerrainTransformer { + public static void transform( + ASTParser t, + TranslationUnit tree, + Root root, + Parameters parameters) { + switch (parameters.type) { + // For Sodium patching, treat fragment and geometry the same + case FRAGMENT: + case GEOMETRY: + transformFragment(t, tree, root, parameters); + break; + case VERTEX: + transformVertex(t, tree, root, parameters); + break; + default: + throw new IllegalStateException("Unexpected Sodium terrain patching shader type: " + parameters.type); + } + } + + private static final AutoHintedMatcher glTextureMatrix0 = new AutoHintedMatcher<>( + "gl_TextureMatrix[0]", Matcher.expressionPattern); + + /** + * Transforms vertex shaders. + */ + public static void transformVertex( + ASTParser t, + TranslationUnit tree, + Root root, + Parameters parameters) { + tree.parseAndInjectNodes(t, ASTInjectionPoint.BEFORE_DECLARATIONS, + "attribute vec3 iris_Pos;", + "attribute vec4 iris_Color;", + "attribute vec2 iris_TexCoord;", + "attribute vec2 iris_LightCoord;", + "attribute vec3 iris_Normal;", // some are shared + "uniform vec3 u_ModelScale;", + "uniform vec2 u_TextureScale;", + "attribute vec4 iris_ModelOffset;", + "vec4 iris_LightTexCoord = vec4(iris_LightCoord, 0, 1);", + "vec4 ftransform() { return gl_ModelViewProjectionMatrix * gl_Vertex; }"); + + transformShared(t, tree, root, parameters); + + root.replaceReferenceExpressions(t, "gl_Vertex", + "vec4((iris_Pos * u_ModelScale) + iris_ModelOffset.xyz, 1.0)"); + root.replaceReferenceExpressions(t, "gl_MultiTexCoord0", + "vec4(iris_TexCoord * u_TextureScale, 0.0, 1.0)"); + root.replaceReferenceExpressions(t, "gl_MultiTexCoord1", + "iris_LightTexCoord"); + root.replaceReferenceExpressions(t, "gl_MultiTexCoord2", + "iris_LightTexCoord"); + root.rename("gl_Color", "iris_Color"); + root.rename("gl_Normal", "iris_Normal"); + root.rename("ftransform", "iris_ftransform"); + } + + /** + * Transforms fragment shaders. The fragment shader does only the shared things + * from the vertex shader. + */ + public static void transformFragment( + ASTParser t, + TranslationUnit tree, + Root root, + Parameters parameters) { + // interestingly there is nothing that isn't shared + transformShared(t, tree, root, parameters); + } + + /** + * Does the things that transformVertex and transformFragment have in common. + */ + private static void transformShared( + ASTParser t, + TranslationUnit tree, + Root root, + Parameters parameters) { + tree.parseAndInjectNodes(t, ASTInjectionPoint.BEFORE_DECLARATIONS, + "uniform mat4 iris_ModelViewMatrix;", + "uniform mat4 u_ModelViewProjectionMatrix;", + "uniform mat4 iris_NormalMatrix;", + "uniform mat4 iris_LightmapTextureMatrix;"); + root.rename("gl_ModelViewMatrix", "iris_ModelViewMatrix"); + root.rename("gl_ModelViewProjectionMatrix", "u_ModelViewProjectionMatrix"); + root.replaceReferenceExpressions(t, + "gl_NormalMatrix", "mat3(iris_NormalMatrix)"); + + root.replaceExpressionMatches( + t, + glTextureMatrix0, + "mat4(1.0)"); + root.replaceExpressionMatches(t, glTextureMatrix1, "iris_LightmapTextureMatrix"); + } + private static final String lightmapCoordsExpression = "iris_LightCoord"; + private static final AutoHintedMatcher glTextureMatrix1 = new AutoHintedMatcher<>( + "gl_TextureMatrix[1]", Matcher.expressionPattern); +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java b/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java new file mode 100644 index 000000000..d9371668d --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java @@ -0,0 +1,226 @@ +package net.coderbot.iris.pipeline.transform; + +import io.github.douira.glsl_transformer.ast.node.Identifier; +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; +import io.github.douira.glsl_transformer.ast.node.Version; +import io.github.douira.glsl_transformer.ast.print.PrintType; +import io.github.douira.glsl_transformer.ast.query.Root; +import io.github.douira.glsl_transformer.ast.transform.EnumASTTransformer; +import io.github.douira.glsl_transformer.cst.core.SemanticException; +import io.github.douira.glsl_transformer.cst.token_filter.ChannelFilter; +import io.github.douira.glsl_transformer.cst.token_filter.TokenChannel; +import io.github.douira.glsl_transformer.cst.token_filter.TokenFilter; +import io.github.douira.glsl_transformer.util.LRUCache; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.pipeline.PatchedShaderPrinter; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Token; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The transform patcher (triforce 2) uses glsl-transformer's ASTTransformer to + * do shader transformation. + * + * The TransformPatcher does caching on the source string and associated + * parameters. For this to work, all objects contained in a parameter must have + * an equals method and they must never be changed after having been used for + * patching. Since the cache also contains the source string, it doesn't need to + * be disabled when developing shaderpacks. However, when changes are made to + * the patcher, the cache should be disabled with {@link #useCache}. + * + * NOTE: This patcher expects (and ensures) that the string doesn't contain any + * (!) preprocessor directives. The only allowed ones are #extension and #pragma + * as they are considered "parsed" directives. If any other directive appears in + * the string, it will throw. + */ +public class TransformPatcher { + static Logger LOGGER = LogManager.getLogger(TransformPatcher.class); + private static EnumASTTransformer transformer; + private static final boolean useCache = true; + private static final Map> cache = new LRUCache<>(400); + + private static class CacheKey { + final Parameters parameters; + final String vertex; + final String geometry; + final String fragment; + + public CacheKey(Parameters parameters, String vertex, String geometry, String fragment) { + this.parameters = parameters; + this.vertex = vertex; + this.geometry = geometry; + this.fragment = fragment; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fragment == null) ? 0 : fragment.hashCode()); + result = prime * result + ((geometry == null) ? 0 : geometry.hashCode()); + result = prime * result + ((parameters == null) ? 0 : parameters.hashCode()); + result = prime * result + ((vertex == null) ? 0 : vertex.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheKey other = (CacheKey) obj; + if (fragment == null) { + if (other.fragment != null) + return false; + } else if (!fragment.equals(other.fragment)) + return false; + if (geometry == null) { + if (other.geometry != null) + return false; + } else if (!geometry.equals(other.geometry)) + return false; + if (parameters == null) { + if (other.parameters != null) + return false; + } else if (!parameters.equals(other.parameters)) + return false; + if (vertex == null) { + if (other.vertex != null) + return false; + } else if (!vertex.equals(other.vertex)) + return false; + return true; + } + } + + // TODO: Only do the NewLines patches if the source code isn't from + // gbuffers_lines (what does this mean?) + + static TokenFilter parseTokenFilter = new ChannelFilter(TokenChannel.PREPROCESSOR) { + @Override + public boolean isTokenAllowed(Token token) { + if (!super.isTokenAllowed(token)) { + throw new SemanticException("Unparsed preprocessor directives such as '" + token.getText() + + "' may not be present at this stage of shader processing!"); + } + return true; + } + }; + + static { + transformer = new EnumASTTransformer(PatchShaderType.class) { + @Override + public TranslationUnit parseTranslationUnit(String input) throws RecognitionException { + // parse #version directive using an efficient regex before parsing so that the + // parser can be set to the correct version + Matcher matcher = versionPattern.matcher(input); + if (!matcher.find()) { + throw new IllegalArgumentException("No #version directive found in source code! See debugging.md for more information."); + } + Version version = Version.fromNumber(Integer.parseInt(matcher.group(1))); + transformer.getLexer().version = version; + + return super.parseTranslationUnit(input); + } + }; + transformer.setTransformation((trees, parameters) -> { + for (PatchShaderType type : PatchShaderType.values()) { + TranslationUnit tree = trees.get(type); + if (tree == null) { + continue; + } + // @Asek3 I have old GLSL-Transformer on JRE8, so I will off this + //tree.outputOptions.enablePrintInfo(); + + parameters.type = type; + Root root = tree.getRoot(); + + // check for illegal references to internal Iris shader interfaces + Optional violation = root.identifierIndex.prefixQueryFlat("iris_").findAny(); + if (!violation.isPresent()) { + violation = root.identifierIndex.prefixQueryFlat("irisMain").findAny(); + } + violation.ifPresent(id -> { + throw new SemanticException( + "Detected a potential reference to unstable and internal Iris shader interfaces (iris_ and irisMain). This isn't currently supported. Violation: " + + id.getName() + ". See debugging.md for more information."); + }); + + Root.indexBuildSession(tree, () -> { + switch (parameters.patch) { + case ATTRIBUTES: + AttributeTransformer.transform(transformer, tree, root, (AttributeParameters) parameters); + break; + case SODIUM_TERRAIN: + SodiumTerrainTransformer.transform(transformer, tree, root, parameters); + break; + case COMPOSITE: + CompositeTransformer.transform(transformer, tree, root); + break; + } + CompatibilityTransformer.transformEach(transformer, tree, root, parameters); + }); + } + + // the compatibility transformer does a grouped transformation + CompatibilityTransformer.transformGrouped(transformer, trees, parameters); + }); + transformer.setParseTokenFilter(parseTokenFilter); + } + + private static final Pattern versionPattern = Pattern.compile("^.*#version\\s+(\\d+)", Pattern.DOTALL); + + private static Map transform(String vertex, String geometry, String fragment, Parameters parameters) { + // stop if all are null + if (vertex == null && geometry == null && fragment == null) { + return null; + } + + // check if this has been cached + CacheKey key; + Map result = null; + if (useCache) { + key = new CacheKey(parameters, vertex, geometry, fragment); + if (cache.containsKey(key)) { + result = cache.get(key); + } + } + + // if there is no cache result, transform the shaders + if (result == null) { + transformer.setPrintType(PatchedShaderPrinter.prettyPrintShaders ? PrintType.INDENTED : PrintType.SIMPLE); + EnumMap inputs = new EnumMap<>(PatchShaderType.class); + inputs.put(PatchShaderType.VERTEX, vertex); + inputs.put(PatchShaderType.GEOMETRY, geometry); + inputs.put(PatchShaderType.FRAGMENT, fragment); + result = transformer.transform(inputs, parameters); + if (useCache) { + cache.put(key, result); + } + } + + return result; + } + + public static Map patchAttributes(String vertex, String geometry, String fragment, InputAvailability inputs) { + return transform(vertex, geometry, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, inputs)); + } + + public static Map patchSodiumTerrain(String vertex, String geometry, String fragment) { + return transform(vertex, geometry, fragment, new Parameters(Patch.SODIUM_TERRAIN)); + } + + public static Map patchComposite(String vertex, String geometry, String fragment) { + return transform(vertex, geometry, fragment, new Parameters(Patch.COMPOSITE)); + } +} diff --git a/src/main/java/net/coderbot/iris/postprocess/BufferFlipper.java b/src/main/java/net/coderbot/iris/postprocess/BufferFlipper.java new file mode 100644 index 000000000..a4b6f32ef --- /dev/null +++ b/src/main/java/net/coderbot/iris/postprocess/BufferFlipper.java @@ -0,0 +1,40 @@ +package net.coderbot.iris.postprocess; + +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; + +public class BufferFlipper { + private final IntSet flippedBuffers; + + public BufferFlipper() { + this.flippedBuffers = new IntOpenHashSet(); + } + + public void flip(int target) { + if (!flippedBuffers.remove(target)) { + // If the target wasn't in the set, add it to the set. + flippedBuffers.add(target); + } + } + + /** + * Returns true if this buffer is flipped. + * + * If this buffer is not flipped, then users should write to the alternate variant and read from the main variant. + * + * If this buffer is flipped, then users should write to the main variant and read from the alternate variant. + */ + public boolean isFlipped(int target) { + return flippedBuffers.contains(target); + } + + public IntIterator getFlippedBuffers() { + return flippedBuffers.iterator(); + } + + public ImmutableSet snapshot() { + return ImmutableSet.copyOf(flippedBuffers); + } +} diff --git a/src/main/java/net/coderbot/iris/postprocess/CenterDepthSampler.java b/src/main/java/net/coderbot/iris/postprocess/CenterDepthSampler.java new file mode 100644 index 000000000..65fc3be24 --- /dev/null +++ b/src/main/java/net/coderbot/iris/postprocess/CenterDepthSampler.java @@ -0,0 +1,124 @@ +package net.coderbot.iris.postprocess; + +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.gl.texture.DepthCopyStrategy; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.gl.texture.PixelType; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.uniforms.SystemTimeUniforms; +import net.minecraft.client.Minecraft; +import org.apache.commons.io.IOUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.function.IntSupplier; + +public class CenterDepthSampler { + private static final double LN2 = Math.log(2); + private boolean hasFirstSample; + private boolean everRetrieved; + private final Program program; + private final GlFramebuffer framebuffer; + private final int texture; + private final int altTexture; + private boolean destroyed; + + public CenterDepthSampler(IntSupplier depthSupplier, float halfLife) { + this.texture = GL11.glGenTextures(); + this.altTexture = GL11.glGenTextures(); + this.framebuffer = new GlFramebuffer(); + + // Fall back to a less precise format if the system doesn't support OpenGL 3 + InternalTextureFormat format = Iris.capabilities.OpenGL32 ? InternalTextureFormat.R32F : InternalTextureFormat.RGB16; + setupColorTexture(texture, format); + setupColorTexture(altTexture, format); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + + this.framebuffer.addColorAttachment(0, texture); + ProgramBuilder builder; + + try { + String fsh = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/centerDepth.fsh"))), StandardCharsets.UTF_8); + + if (Iris.capabilities.OpenGL32) { + fsh = fsh.replace("VERSIONPLACEHOLDER", "150 compatibility"); + } else { + fsh = fsh.replace("#define IS_GL3", ""); + fsh = fsh.replace("VERSIONPLACEHOLDER", "120"); + } + String vsh = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/centerDepth.vsh"))), StandardCharsets.UTF_8); + + builder = ProgramBuilder.begin("centerDepthSmooth", vsh, null, fsh, ImmutableSet.of(0, 1, 2)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + builder.addDynamicSampler(depthSupplier, "depth"); + builder.addDynamicSampler(() -> altTexture, "altDepth"); + builder.uniform1f(UniformUpdateFrequency.PER_FRAME, "lastFrameTime", SystemTimeUniforms.TIMER::getLastFrameTime); + builder.uniform1f(UniformUpdateFrequency.ONCE, "decay", () -> (1.0f / ((halfLife * 0.1) / LN2))); + this.program = builder.build(); + } + + public void sampleCenterDepth() { + if ((hasFirstSample && (!everRetrieved)) || destroyed) { + // If the shaderpack isn't reading center depth values, don't bother sampling it + // This improves performance with most shaderpacks + return; + } + + hasFirstSample = true; + + this.framebuffer.bind(); + this.program.use(); + + GL11.glViewport(0, 0, 1, 1); + + FullScreenQuadRenderer.INSTANCE.render(); + + ProgramUniforms.clearActiveUniforms(); + ProgramSamplers.clearActiveSamplers(); + + // The API contract of DepthCopyStrategy claims it can only copy depth, however the 2 non-stencil methods used are entirely capable of copying color as of now. + DepthCopyStrategy.fastest(false).copy(this.framebuffer, texture, null, altTexture, 1, 1); + + //Reset viewport + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(true); + } + + public void setupColorTexture(int texture, InternalTextureFormat format) { + IrisRenderSystem.texImage2D(texture, GL11.GL_TEXTURE_2D, 0, format.getGlFormat(), 1, 1, 0, format.getPixelFormat().getGlFormat(), PixelType.FLOAT.getGlFormat(), null); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + } + + public int getCenterDepthTexture() { + return altTexture; + } + + public void setUsage(boolean usage) { + everRetrieved |= usage; + } + + public void destroy() { + GLStateManager.glDeleteTextures(texture); + GLStateManager.glDeleteTextures(altTexture); + framebuffer.destroy(); + program.destroy(); + destroyed = true; + } +} diff --git a/src/main/java/net/coderbot/iris/postprocess/CompositeRenderer.java b/src/main/java/net/coderbot/iris/postprocess/CompositeRenderer.java new file mode 100644 index 000000000..3780d5186 --- /dev/null +++ b/src/main/java/net/coderbot/iris/postprocess/CompositeRenderer.java @@ -0,0 +1,386 @@ +package net.coderbot.iris.postprocess; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import lombok.Getter; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.program.ComputeProgram; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.gl.sampler.SamplerLimits; +import net.coderbot.iris.pipeline.PatchedShaderPrinter; +import net.coderbot.iris.pipeline.transform.PatchShaderType; +import net.coderbot.iris.pipeline.transform.TransformPatcher; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.samplers.IrisImages; +import net.coderbot.iris.samplers.IrisSamplers; +import net.coderbot.iris.shaderpack.ComputeSource; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import net.coderbot.iris.shaderpack.ProgramDirectives; +import net.coderbot.iris.shaderpack.ProgramSource; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import net.coderbot.iris.uniforms.CommonUniforms; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; + +import java.util.Map; +import java.util.Objects; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class CompositeRenderer { + private final RenderTargets renderTargets; + + private final ImmutableList passes; + private final IntSupplier noiseTexture; + private final FrameUpdateNotifier updateNotifier; + private final CenterDepthSampler centerDepthSampler; + private final Object2ObjectMap customTextureIds; + @Getter + private final ImmutableSet flippedAtLeastOnceFinal; + + public CompositeRenderer(PackDirectives packDirectives, ProgramSource[] sources, ComputeSource[][] computes, RenderTargets renderTargets, + IntSupplier noiseTexture, FrameUpdateNotifier updateNotifier, + CenterDepthSampler centerDepthSampler, BufferFlipper bufferFlipper, + Supplier shadowTargetsSupplier, + Object2ObjectMap customTextureIds, ImmutableMap explicitPreFlips) { + this.noiseTexture = noiseTexture; + this.updateNotifier = updateNotifier; + this.centerDepthSampler = centerDepthSampler; + this.renderTargets = renderTargets; + this.customTextureIds = customTextureIds; + + final PackRenderTargetDirectives renderTargetDirectives = packDirectives.getRenderTargetDirectives(); + final Map renderTargetSettings = renderTargetDirectives.getRenderTargetSettings(); + + final ImmutableList.Builder passes = ImmutableList.builder(); + final ImmutableSet.Builder flippedAtLeastOnce = new ImmutableSet.Builder<>(); + + explicitPreFlips.forEach((buffer, shouldFlip) -> { + if (shouldFlip) { + bufferFlipper.flip(buffer); + // NB: Flipping deferred_pre or composite_pre does NOT cause the "flippedAtLeastOnce" flag to trigger + } + }); + + for (int i = 0; i < sources.length; i++) { + final ProgramSource source = sources[i]; + + ImmutableSet flipped = bufferFlipper.snapshot(); + ImmutableSet flippedAtLeastOnceSnapshot = flippedAtLeastOnce.build(); + + if (source == null || !source.isValid()) { + if (computes[i] != null) { + ComputeOnlyPass pass = new ComputeOnlyPass(); + pass.computes = createComputes(computes[i], flipped, flippedAtLeastOnceSnapshot, shadowTargetsSupplier); + passes.add(pass); + } + continue; + } + + Pass pass = new Pass(); + ProgramDirectives directives = source.getDirectives(); + + pass.program = createProgram(source, flipped, flippedAtLeastOnceSnapshot, shadowTargetsSupplier); + pass.computes = createComputes(computes[i], flipped, flippedAtLeastOnceSnapshot, shadowTargetsSupplier); + int[] drawBuffers = directives.getDrawBuffers(); + + GlFramebuffer framebuffer = renderTargets.createColorFramebuffer(flipped, drawBuffers); + + int passWidth = 0, passHeight = 0; + // Flip the buffers that this shader wrote to, and set pass width and height + ImmutableMap explicitFlips = directives.getExplicitFlips(); + + for (int buffer : drawBuffers) { + RenderTarget target = renderTargets.get(buffer); + if ((passWidth > 0 && passWidth != target.getWidth()) || (passHeight > 0 && passHeight != target.getHeight())) { + throw new IllegalStateException("Pass widths must match"); + } + passWidth = target.getWidth(); + passHeight = target.getHeight(); + + // compare with boxed Boolean objects to avoid NPEs + if (explicitFlips.get(buffer) == Boolean.FALSE) { + continue; + } + + bufferFlipper.flip(buffer); + flippedAtLeastOnce.add(buffer); + } + + explicitFlips.forEach((buffer, shouldFlip) -> { + if (shouldFlip) { + bufferFlipper.flip(buffer); + flippedAtLeastOnce.add(buffer); + } + }); + + pass.drawBuffers = directives.getDrawBuffers(); + pass.viewWidth = passWidth; + pass.viewHeight = passHeight; + pass.stageReadsFromAlt = flipped; + pass.framebuffer = framebuffer; + pass.viewportScale = directives.getViewportScale(); + pass.mipmappedBuffers = directives.getMipmappedBuffers(); + pass.flippedAtLeastOnce = flippedAtLeastOnceSnapshot; + + passes.add(pass); + } + + this.passes = passes.build(); + this.flippedAtLeastOnceFinal = flippedAtLeastOnce.build(); + + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, 0); + } + + public void recalculateSizes() { + for (Pass pass : passes) { + if (pass instanceof ComputeOnlyPass) { + continue; + } + int passWidth = 0, passHeight = 0; + for (int buffer : pass.drawBuffers) { + final RenderTarget target = renderTargets.get(buffer); + if ((passWidth > 0 && passWidth != target.getWidth()) || (passHeight > 0 && passHeight != target.getHeight())) { + throw new IllegalStateException("Pass widths must match"); + } + passWidth = target.getWidth(); + passHeight = target.getHeight(); + } + renderTargets.destroyFramebuffer(pass.framebuffer); + pass.framebuffer = renderTargets.createColorFramebuffer(pass.stageReadsFromAlt, pass.drawBuffers); + pass.viewWidth = passWidth; + pass.viewHeight = passHeight; + } + } + + private static class Pass { + int[] drawBuffers; + int viewWidth; + int viewHeight; + Program program; + ComputeProgram[] computes; + GlFramebuffer framebuffer; + ImmutableSet flippedAtLeastOnce; + ImmutableSet stageReadsFromAlt; + ImmutableSet mipmappedBuffers; + float viewportScale; + + protected void destroy() { + this.program.destroy(); + for (ComputeProgram compute : this.computes) { + if (compute != null) { + compute.destroy(); + } + } + } + } + + private class ComputeOnlyPass extends Pass { + @Override + protected void destroy() { + for (ComputeProgram compute : this.computes) { + if (compute != null) { + compute.destroy(); + } + } + } + } + + public void renderAll() { + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + + FullScreenQuadRenderer.INSTANCE.begin(); + + for (Pass renderPass : passes) { + boolean ranCompute = false; + for (ComputeProgram computeProgram : renderPass.computes) { + if (computeProgram != null) { + ranCompute = true; + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + computeProgram.dispatch(main.framebufferWidth, main.framebufferHeight); + } + } + + if (ranCompute) { + IrisRenderSystem.memoryBarrier(40); + } + + Program.unbind(); + + if (renderPass instanceof ComputeOnlyPass) { + continue; + } + + if (!renderPass.mipmappedBuffers.isEmpty()) { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + for (int index : renderPass.mipmappedBuffers) { + setupMipmapping(CompositeRenderer.this.renderTargets.get(index), renderPass.stageReadsFromAlt.contains(index)); + } + } + + final float scaledWidth = renderPass.viewWidth * renderPass.viewportScale; + final float scaledHeight = renderPass.viewHeight * renderPass.viewportScale; + GL11.glViewport(0, 0, (int) scaledWidth, (int) scaledHeight); + + renderPass.framebuffer.bind(); + renderPass.program.use(); + + FullScreenQuadRenderer.INSTANCE.renderQuad(); + } + + FullScreenQuadRenderer.end(); + + // Make sure to reset the viewport to how it was before... Otherwise weird issues could occur. + // Also bind the "main" framebuffer if it isn't already bound. + Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(true); + ProgramUniforms.clearActiveUniforms(); + ProgramSamplers.clearActiveSamplers(); + GL20.glUseProgram(0); + + // NB: Unbinding all of these textures is necessary for proper shaderpack reloading. + for (int i = 0; i < SamplerLimits.get().getMaxTextureUnits(); i++) { + // Unbind all textures that we may have used. + // NB: This is necessary for shader pack reloading to work propely + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } + + private static void setupMipmapping(RenderTarget target, boolean readFromAlt) { + int texture = readFromAlt ? target.getAltTexture() : target.getMainTexture(); + + // TODO: Only generate the mipmap if a valid mipmap hasn't been generated or if we've written to the buffer + // (since the last mipmap was generated) + // + // NB: We leave mipmapping enabled even if the buffer is written to again, this appears to match the + // behavior of ShadersMod/OptiFine, however I'm not sure if it's desired behavior. It's possible that a + // program could use mipmapped sampling with a stale mipmap, which probably isn't great. However, the + // sampling mode is always reset between frames, so this only persists after the first program to use + // mipmapping on this buffer. + // + // Also note that this only applies to one of the two buffers in a render target buffer pair - making it + // unlikely that this issue occurs in practice with most shader packs. + IrisRenderSystem.generateMipmaps(texture, GL11.GL_TEXTURE_2D); + + int filter = GL11.GL_LINEAR_MIPMAP_LINEAR; + if (target.getInternalFormat().getPixelFormat().isInteger()) { + filter = GL11.GL_NEAREST_MIPMAP_NEAREST; + } + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); + } + + // TODO: Don't just copy this from DeferredWorldRenderingPipeline + private Program createProgram(ProgramSource source, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, + Supplier shadowTargetsSupplier) { + // TODO: Properly handle empty shaders + Map transformed = TransformPatcher.patchComposite( + source.getVertexSource().orElseThrow(NullPointerException::new), + source.getGeometrySource().orElse(null), + source.getFragmentSource().orElseThrow(NullPointerException::new)); + String vertex = transformed.get(PatchShaderType.VERTEX); + String geometry = transformed.get(PatchShaderType.GEOMETRY); + String fragment = transformed.get(PatchShaderType.FRAGMENT); + PatchedShaderPrinter.debugPatchedShaders(source.getName(), vertex, geometry, fragment); + + Objects.requireNonNull(flipped); + ProgramBuilder builder; + + try { + builder = ProgramBuilder.begin(source.getName(), vertex, geometry, fragment, + IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); + } catch (RuntimeException e) { + // TODO: Better error handling + throw new RuntimeException("Shader compilation failed!", e); + } + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); + + CommonUniforms.addCommonUniforms(builder, source.getParent().getPack().getIdMap(), source.getParent().getPackDirectives(), updateNotifier); + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true); + IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); + + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); + IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get()); + IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get()); + } + + // TODO: Don't duplicate this with FinalPassRenderer + centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); + + return builder.build(); + } + + private ComputeProgram[] createComputes(ComputeSource[] compute, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, Supplier shadowTargetsSupplier) { + ComputeProgram[] programs = new ComputeProgram[compute.length]; + for (int i = 0; i < programs.length; i++) { + ComputeSource source = compute[i]; + if (source == null || !source.getSource().isPresent()) { + continue; + } else { + // TODO: Properly handle empty shaders + Objects.requireNonNull(flipped); + ProgramBuilder builder; + + try { + builder = ProgramBuilder.beginCompute(source.getName(), source.getSource().orElse(null), IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); + } catch (RuntimeException e) { + // TODO: Better error handling + throw new RuntimeException("Shader compilation failed!", e); + } + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); + + CommonUniforms.addCommonUniforms(builder, source.getParent().getPack().getIdMap(), source.getParent().getPackDirectives(), updateNotifier); + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true); + IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); + + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); + IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get()); + IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get()); + } + + // TODO: Don't duplicate this with FinalPassRenderer + centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); + + programs[i] = builder.buildCompute(); + + programs[i].setWorkGroupInfo(source.getWorkGroupRelative(), source.getWorkGroups()); + } + } + + + return programs; + } + + public void destroy() { + for (Pass renderPass : passes) { + renderPass.destroy(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/postprocess/FinalPassRenderer.java b/src/main/java/net/coderbot/iris/postprocess/FinalPassRenderer.java new file mode 100644 index 000000000..e68a7d472 --- /dev/null +++ b/src/main/java/net/coderbot/iris/postprocess/FinalPassRenderer.java @@ -0,0 +1,403 @@ +package net.coderbot.iris.postprocess; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.program.ComputeProgram; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.gl.sampler.SamplerLimits; +import net.coderbot.iris.pipeline.PatchedShaderPrinter; +import net.coderbot.iris.pipeline.transform.PatchShaderType; +import net.coderbot.iris.pipeline.transform.TransformPatcher; +import net.coderbot.iris.rendertarget.IRenderTargetExt; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.samplers.IrisImages; +import net.coderbot.iris.samplers.IrisSamplers; +import net.coderbot.iris.shaderpack.ComputeSource; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import net.coderbot.iris.shaderpack.ProgramDirectives; +import net.coderbot.iris.shaderpack.ProgramSet; +import net.coderbot.iris.shaderpack.ProgramSource; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import net.coderbot.iris.uniforms.CommonUniforms; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; + +import java.util.Map; +import java.util.Objects; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class FinalPassRenderer { + private final RenderTargets renderTargets; + + @Nullable + private final Pass finalPass; + private final ImmutableList swapPasses; + private final GlFramebuffer baseline; + private final GlFramebuffer colorHolder; + private int lastColorTextureId; + private int lastColorTextureVersion; + private final IntSupplier noiseTexture; + private final FrameUpdateNotifier updateNotifier; + private final CenterDepthSampler centerDepthSampler; + private final Object2ObjectMap customTextureIds; + + // TODO: The length of this argument list is getting a bit ridiculous + public FinalPassRenderer(ProgramSet pack, RenderTargets renderTargets, IntSupplier noiseTexture, + FrameUpdateNotifier updateNotifier, ImmutableSet flippedBuffers, + CenterDepthSampler centerDepthSampler, + Supplier shadowTargetsSupplier, + Object2ObjectMap customTextureIds, + ImmutableSet flippedAtLeastOnce) { + this.updateNotifier = updateNotifier; + this.centerDepthSampler = centerDepthSampler; + this.customTextureIds = customTextureIds; + + final PackRenderTargetDirectives renderTargetDirectives = pack.getPackDirectives().getRenderTargetDirectives(); + final Map renderTargetSettings = renderTargetDirectives.getRenderTargetSettings(); + + this.noiseTexture = noiseTexture; + this.renderTargets = renderTargets; + this.finalPass = pack.getCompositeFinal().map(source -> { + Pass pass = new Pass(); + ProgramDirectives directives = source.getDirectives(); + + pass.program = createProgram(source, flippedBuffers, flippedAtLeastOnce, shadowTargetsSupplier); + pass.computes = createComputes(pack.getFinalCompute(), flippedBuffers, flippedAtLeastOnce, shadowTargetsSupplier); + pass.stageReadsFromAlt = flippedBuffers; + pass.mipmappedBuffers = directives.getMipmappedBuffers(); + + return pass; + }).orElse(null); + + IntList buffersToBeCleared = pack.getPackDirectives().getRenderTargetDirectives().getBuffersToBeCleared(); + + // The name of this method might seem a bit odd here, but we want a framebuffer with color attachments that line + // up with whatever was written last (since we're reading from these framebuffers) instead of trying to create + // a framebuffer with color attachments different from what was written last (as we do with normal composite + // passes that write to framebuffers). + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + this.baseline = renderTargets.createGbufferFramebuffer(flippedBuffers, new int[] {0}); + this.colorHolder = new GlFramebuffer(); + this.lastColorTextureId = main.framebufferTexture; + this.lastColorTextureVersion = ((IRenderTargetExt)main).iris$getColorBufferVersion(); + this.colorHolder.addColorAttachment(0, lastColorTextureId); + + // TODO: We don't actually fully swap the content, we merely copy it from alt to main + // This works for the most part, but it's not perfect. A better approach would be creating secondary + // framebuffers for every other frame, but that would be a lot more complex... + ImmutableList.Builder swapPasses = ImmutableList.builder(); + + flippedBuffers.forEach(i -> { + int target = i; + + if (buffersToBeCleared.contains(target)) { + return; + } + + SwapPass swap = new SwapPass(); + RenderTarget target1 = renderTargets.get(target); + swap.target = target; + swap.width = target1.getWidth(); + swap.height = target1.getHeight(); + swap.from = renderTargets.createColorFramebuffer(ImmutableSet.of(), new int[] {target}); + // NB: This is handled in RenderTargets now. + //swap.from.readBuffer(target); + swap.targetTexture = renderTargets.get(target).getMainTexture(); + + swapPasses.add(swap); + }); + + this.swapPasses = swapPasses.build(); + + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, 0); + } + + private static final class Pass { + Program program; + ComputeProgram[] computes; + ImmutableSet stageReadsFromAlt; + ImmutableSet mipmappedBuffers; + + private void destroy() { + this.program.destroy(); + } + } + + private static final class SwapPass { + public int target; + public int width; + public int height; + GlFramebuffer from; + int targetTexture; + } + + public void renderFinalPass() { + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + GLStateManager.glDepthMask(false); + + final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); + final int baseWidth = main.framebufferWidth; + final int baseHeight = main.framebufferHeight; + + // Note that since DeferredWorldRenderingPipeline uses the depth texture of the main Minecraft framebuffer, + // we'll be writing to that depth buffer directly automatically and won't need to futz around with copying + // depth buffer content. + // + // Previously, we had our own depth texture and then copied its content to the main Minecraft framebuffer. + // This worked with vanilla, but broke with mods that used the stencil buffer. + // + // This approach is a fairly succinct solution to the issue of having to deal with the main Minecraft + // framebuffer potentially having a depth-stencil buffer or similar - we'll automatically enable that to + // work properly since we re-use the depth buffer instead of trying to make our own. + // + // This is not a concern for depthtex1 / depthtex2 since the copy call extracts the depth values, and the + // shader pack only ever uses them to read the depth values. + if (((IRenderTargetExt)main).iris$getColorBufferVersion() != lastColorTextureVersion || main.framebufferTexture != lastColorTextureId) { + lastColorTextureVersion = ((IRenderTargetExt)main).iris$getColorBufferVersion(); + this.lastColorTextureId = main.framebufferTexture; + colorHolder.addColorAttachment(0, lastColorTextureId); + } + + if (this.finalPass != null) { + // If there is a final pass, we use the shader-based full screen quad rendering pathway instead of just copying the color buffer. + + colorHolder.bind(); + + FullScreenQuadRenderer.INSTANCE.begin(); + + for (ComputeProgram computeProgram : finalPass.computes) { + if (computeProgram != null) { + computeProgram.dispatch(baseWidth, baseHeight); + } + } + + IrisRenderSystem.memoryBarrier(40); + + if (!finalPass.mipmappedBuffers.isEmpty()) { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + for (int index : finalPass.mipmappedBuffers) { + setupMipmapping(renderTargets.get(index), finalPass.stageReadsFromAlt.contains(index)); + } + } + + finalPass.program.use(); + FullScreenQuadRenderer.INSTANCE.renderQuad(); + + FullScreenQuadRenderer.end(); + } else { + // If there are no passes, we somehow need to transfer the content of the Iris color render targets into + // the main Minecraft framebuffer. + // + // Thus, the following call transfers the content of colortex0 into the main Minecraft framebuffer. + // + // Note that glCopyTexSubImage2D is not as strict as glBlitFramebuffer, so we don't have to worry about + // colortex0 having a weird format. This should just work. + // + // We could have used a shader here, but it should be about the same performance either way: + // https://stackoverflow.com/a/23994979/18166885 + this.baseline.bindAsReadBuffer(); + + IrisRenderSystem.copyTexSubImage2D(main.framebufferTexture, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, baseWidth, baseHeight); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + for (int i = 0; i < renderTargets.getRenderTargetCount(); i++) { + // Reset mipmapping states at the end of the frame. + resetRenderTarget(renderTargets.get(i)); + } + + for (SwapPass swapPass : swapPasses) { + // NB: We need to use bind(), not bindAsReadBuffer()... Previously we used bindAsReadBuffer() here which + // broke TAA on many packs and on many drivers. + // + // Note that glCopyTexSubImage2D reads from the current GL_READ_BUFFER (given by glReadBuffer()) for the + // current framebuffer bound to GL_FRAMEBUFFER, but that is distinct from the current GL_READ_FRAMEBUFFER, + // which is what bindAsReadBuffer() binds. + // + // Also note that RenderTargets already calls readBuffer(0) for us. + swapPass.from.bind(); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, swapPass.targetTexture); + GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, swapPass.width, swapPass.height); + } + + // Make sure to reset the viewport to how it was before... Otherwise weird issues could occur. + // Also bind the "main" framebuffer if it isn't already bound. + main.bindFramebuffer(true); + ProgramUniforms.clearActiveUniforms(); + ProgramSamplers.clearActiveSamplers(); + GL20.glUseProgram(0); + + for (int i = 0; i < SamplerLimits.get().getMaxTextureUnits(); i++) { + // Unbind all textures that we may have used. + // NB: This is necessary for shader pack reloading to work properly + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } + + public void recalculateSwapPassSize() { + for (SwapPass swapPass : swapPasses) { + RenderTarget target = renderTargets.get(swapPass.target); + renderTargets.destroyFramebuffer(swapPass.from); + swapPass.from = renderTargets.createColorFramebuffer(ImmutableSet.of(), new int[] {swapPass.target}); + swapPass.width = target.getWidth(); + swapPass.height = target.getHeight(); + swapPass.targetTexture = target.getMainTexture(); + } + } + + private static void setupMipmapping(RenderTarget target, boolean readFromAlt) { + int texture = readFromAlt ? target.getAltTexture() : target.getMainTexture(); + + // TODO: Only generate the mipmap if a valid mipmap hasn't been generated or if we've written to the buffer + // (since the last mipmap was generated) + // + // NB: We leave mipmapping enabled even if the buffer is written to again, this appears to match the + // behavior of ShadersMod/OptiFine, however I'm not sure if it's desired behavior. It's possible that a + // program could use mipmapped sampling with a stale mipmap, which probably isn't great. However, the + // sampling mode is always reset between frames, so this only persists after the first program to use + // mipmapping on this buffer. + // + // Also note that this only applies to one of the two buffers in a render target buffer pair - making it + // unlikely that this issue occurs in practice with most shader packs. + IrisRenderSystem.generateMipmaps(texture, GL11.GL_TEXTURE_2D); + + int filter = GL11.GL_LINEAR_MIPMAP_LINEAR; + if (target.getInternalFormat().getPixelFormat().isInteger()) { + filter = GL11.GL_NEAREST_MIPMAP_NEAREST; + } + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); + } + + private static void resetRenderTarget(RenderTarget target) { + // Resets the sampling mode of the given render target and then unbinds it to prevent accidental sampling of it + // elsewhere. + int filter = GL11.GL_LINEAR; + if (target.getInternalFormat().getPixelFormat().isInteger()) { + filter = GL11.GL_NEAREST; + } + + IrisRenderSystem.texParameteri(target.getMainTexture(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); + IrisRenderSystem.texParameteri(target.getAltTexture(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + // TODO: Don't just copy this from DeferredWorldRenderingPipeline + private Program createProgram(ProgramSource source, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, + Supplier shadowTargetsSupplier) { + // TODO: Properly handle empty shaders + Map transformed = TransformPatcher.patchComposite( + source.getVertexSource().orElseThrow(NullPointerException::new), + source.getGeometrySource().orElse(null), + source.getFragmentSource().orElseThrow(NullPointerException::new)); + String vertex = transformed.get(PatchShaderType.VERTEX); + String geometry = transformed.get(PatchShaderType.GEOMETRY); + String fragment = transformed.get(PatchShaderType.FRAGMENT); + PatchedShaderPrinter.debugPatchedShaders(source.getName(), vertex, geometry, fragment); + + Objects.requireNonNull(flipped); + ProgramBuilder builder; + + try { + builder = ProgramBuilder.begin(source.getName(), vertex, geometry, fragment, + IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); + } catch (RuntimeException e) { + // TODO: Better error handling + throw new RuntimeException("Shader compilation failed!", e); + } + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); + + CommonUniforms.addCommonUniforms(builder, source.getParent().getPack().getIdMap(), source.getParent().getPackDirectives(), updateNotifier); + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true); + IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); + IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get()); + IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get()); + } + + // TODO: Don't duplicate this with CompositeRenderer + centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); + + return builder.build(); + } + + private ComputeProgram[] createComputes(ComputeSource[] compute, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, Supplier shadowTargetsSupplier) { + ComputeProgram[] programs = new ComputeProgram[compute.length]; + for (int i = 0; i < programs.length; i++) { + ComputeSource source = compute[i]; + if (source == null || !source.getSource().isPresent()) { + continue; + } else { + // TODO: Properly handle empty shaders + Objects.requireNonNull(flipped); + ProgramBuilder builder; + + try { + builder = ProgramBuilder.beginCompute(source.getName(), source.getSource().orElse(null), IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); + } catch (RuntimeException e) { + // TODO: Better error handling + throw new RuntimeException("Shader compilation failed!", e); + } + + ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); + + CommonUniforms.addCommonUniforms(builder, source.getParent().getPack().getIdMap(), source.getParent().getPackDirectives(), updateNotifier); + IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true); + IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); + + IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); + IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); + + if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { + IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get()); + IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get()); + } + + // TODO: Don't duplicate this with FinalPassRenderer + centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); + + programs[i] = builder.buildCompute(); + + programs[i].setWorkGroupInfo(source.getWorkGroupRelative(), source.getWorkGroups()); + } + } + + + return programs; + } + + public void destroy() { + if (finalPass != null) { + finalPass.destroy(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/postprocess/FullScreenQuadRenderer.java b/src/main/java/net/coderbot/iris/postprocess/FullScreenQuadRenderer.java new file mode 100644 index 000000000..c79d797bc --- /dev/null +++ b/src/main/java/net/coderbot/iris/postprocess/FullScreenQuadRenderer.java @@ -0,0 +1,87 @@ +package net.coderbot.iris.postprocess; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.IrisRenderSystem; +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; + +import java.nio.FloatBuffer; + +/** + * Renders a full-screen textured quad to the screen. Used in composite / deferred rendering. + */ +public class FullScreenQuadRenderer { + private final int quadBuffer; + + public static final FullScreenQuadRenderer INSTANCE = new FullScreenQuadRenderer(); + + private FullScreenQuadRenderer() { + this.quadBuffer = createQuad(); + } + + public void render() { + begin(); + + renderQuad(); + + end(); + } + + @SuppressWarnings("deprecation") + public void begin() { + GLStateManager.disableDepthTest(); + + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + // scale the quad from [0, 1] to [-1, 1] + GL11.glTranslatef(-1.0F, -1.0F, 0.0F); + GL11.glScalef(2.0F, 2.0F, 0.0F); + + GL11.glMatrixMode(GL11.GL_MODELVIEW); + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, quadBuffer); + DefaultVertexFormat.POSITION_TEXTURE.setupBufferState(0L); + } + + public void renderQuad() { + GLStateManager.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0, 4); + } + + @SuppressWarnings("deprecation") + public static void end() { + DefaultVertexFormat.POSITION_TEXTURE.clearBufferState(); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + GLStateManager.enableDepthTest(); + + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPopMatrix(); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + GL11.glPopMatrix(); + } + + /** + * Creates and uploads a vertex buffer containing a single full-screen quad + */ + private static int createQuad() { + final FloatBuffer vertices = BufferUtils.createFloatBuffer(20); + vertices.put(new float[] { + // Vertex 0: Top right corner + 1.0F, 1.0F, 0.0F, 1.0F, 1.0F, + // Vertex 1: Top left corner + 0.0F, 1.0F, 0.0F, 0.0F, 1.0F, + // Vertex 2: Bottom right corner + 1.0F, 0.0F, 0.0F, 1.0F, 0.0F, + // Vertex 3: Bottom left corner + 0.0F, 0.0F, 0.0F, 0.0F, 0.0F }).rewind(); + + return IrisRenderSystem.bufferStorage(GL15.GL_ARRAY_BUFFER, vertices, GL15.GL_STATIC_DRAW); + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/ColorTexture.java b/src/main/java/net/coderbot/iris/rendertarget/ColorTexture.java new file mode 100644 index 000000000..9680bd4e2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/ColorTexture.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.TextureUploadHelper; +import org.lwjgl.opengl.EXTFramebufferObject; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.nio.ByteBuffer; + +public class ColorTexture extends GlResource { + public ColorTexture(int width, int height) { + super(IrisRenderSystem.createTexture(GL11.GL_TEXTURE_2D)); + final int texture = getGlId(); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + TextureUploadHelper.resetTextureUploadState(); + IrisRenderSystem.texImage2D(texture, GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, (ByteBuffer) null); + + EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, texture, 0); + } + + public int getTextureId() { + return getGlId(); + } + + @Override + protected void destroyInternal() { + GLStateManager.glDeleteTextures(getGlId()); + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/DepthTexture.java b/src/main/java/net/coderbot/iris/rendertarget/DepthTexture.java new file mode 100644 index 000000000..9906002d4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/DepthTexture.java @@ -0,0 +1,38 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.DepthBufferFormat; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +public class DepthTexture extends GlResource { + public DepthTexture(int width, int height, DepthBufferFormat format) { + super(IrisRenderSystem.createTexture(GL11.GL_TEXTURE_2D)); + final int texture = getGlId(); + + resize(width, height, format); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + void resize(int width, int height, DepthBufferFormat format) { + IrisRenderSystem.texImage2D(getTextureId(), GL11.GL_TEXTURE_2D, 0, format.getGlInternalFormat(), width, height, 0, + format.getGlType(), format.getGlFormat(), null); + } + + public int getTextureId() { + return getGlId(); + } + + @Override + protected void destroyInternal() { + GLStateManager.glDeleteTextures(getGlId()); + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/IRenderTargetExt.java b/src/main/java/net/coderbot/iris/rendertarget/IRenderTargetExt.java new file mode 100644 index 000000000..976e62efc --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/IRenderTargetExt.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.rendertarget; + +public interface IRenderTargetExt { + int iris$getDepthBufferVersion(); + + int iris$getColorBufferVersion(); + + public boolean getIris$useDepth(); + public int getIris$depthTextureId(); +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedCustomTexture.java b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedCustomTexture.java new file mode 100644 index 000000000..22d6e7f6d --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedCustomTexture.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.shaderpack.texture.CustomTextureData; +import net.minecraft.client.renderer.texture.DynamicTexture; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class NativeImageBackedCustomTexture extends DynamicTexture { + public NativeImageBackedCustomTexture(CustomTextureData.PngData textureData) throws IOException { + super(create(textureData.getContent())); + + // By default, images are unblurred and not clamped. + + if (textureData.getFilteringData().shouldBlur()) { + IrisRenderSystem.texParameteri(getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + } + + if (textureData.getFilteringData().shouldClamp()) { + IrisRenderSystem.texParameteri(getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + IrisRenderSystem.texParameteri(getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + } + } + + private static NativeImage create(byte[] content) throws IOException { + ByteBuffer buffer = ByteBuffer.allocateDirect(content.length); + buffer.put(content); + buffer.flip(); + + return NativeImage.read(buffer); + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedNoiseTexture.java b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedNoiseTexture.java new file mode 100644 index 000000000..fbb7c232c --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedNoiseTexture.java @@ -0,0 +1,36 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import net.minecraft.client.renderer.texture.DynamicTexture; +import org.lwjgl.opengl.GL11; + +import java.util.Random; + +public class NativeImageBackedNoiseTexture extends DynamicTexture { + public NativeImageBackedNoiseTexture(int size) { + super(create(size)); + } + + private static NativeImage create(int size) { + NativeImage image = new NativeImage(NativeImage.Format.RGBA, size, size, false); + Random random = new Random(0); + + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + int color = random.nextInt() | (255 << 24); + + image.setPixelRGBA(x, y, color); + } + } + + return image; + } + + @Override + public void updateDynamicTexture() { + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + super.updateDynamicTexture(); + } + +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedSingleColorTexture.java b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedSingleColorTexture.java new file mode 100644 index 000000000..2671804d8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/NativeImageBackedSingleColorTexture.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import net.minecraft.client.renderer.texture.DynamicTexture; + +public class NativeImageBackedSingleColorTexture extends DynamicTexture { + public NativeImageBackedSingleColorTexture(int red, int green, int blue, int alpha) { + super(create(NativeImage.combine(alpha, blue, green, red))); + } + + public NativeImageBackedSingleColorTexture(int rgba) { + this(rgba >> 24 & 0xFF, rgba >> 16 & 0xFF, rgba >> 8 & 0xFF, rgba & 0xFF); + } + + private static NativeImage create(int color) { + NativeImage image = new NativeImage(NativeImage.Format.RGBA, 1, 1, false); + + image.setPixelRGBA(0, 0, color); + + return image; + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/NoiseTexture.java b/src/main/java/net/coderbot/iris/rendertarget/NoiseTexture.java new file mode 100644 index 000000000..db1780a4c --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/NoiseTexture.java @@ -0,0 +1,76 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.TextureUploadHelper; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL14; + +import java.nio.ByteBuffer; +import java.util.Random; + +/** + * An extremely simple noise texture. Each color channel contains a uniform random value from 0 to 255. Essentially just + * dumps an array of random bytes into a texture and calls it a day, literally could not be any simpler than that. + */ +public class NoiseTexture extends GlResource { + int width; + int height; + + public NoiseTexture(int width, int height) { + super(IrisRenderSystem.createTexture(GL11.GL_TEXTURE_2D)); + + int texture = getGlId(); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MIN_LOD, 0); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LOD,0); + IrisRenderSystem.texParameterf(texture, GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS, 0.0F); + resize(texture, width, height); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + void resize(int texture, int width, int height) { + this.width = width; + this.height = height; + + ByteBuffer pixels = generateNoise(); + + TextureUploadHelper.resetTextureUploadState(); + + // Since we're using tightly-packed RGB data, we must use an alignment of 1 byte instead of the usual 4 bytes. + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); + IrisRenderSystem.texImage2D(texture, GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, pixels); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + private ByteBuffer generateNoise() { + byte[] pixels = new byte[3 * width * height]; + + Random random = new Random(0); + random.nextBytes(pixels); + + ByteBuffer buffer = ByteBuffer.allocateDirect(pixels.length); + buffer.put(pixels); + buffer.flip(); + + return buffer; + } + + public int getTextureId() { + return getGlId(); + } + + @Override + protected void destroyInternal() { + GLStateManager.glDeleteTextures(getGlId()); + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/RenderTarget.java b/src/main/java/net/coderbot/iris/rendertarget/RenderTarget.java new file mode 100644 index 000000000..db8f3dde4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/RenderTarget.java @@ -0,0 +1,166 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.gl.texture.PixelFormat; +import net.coderbot.iris.gl.texture.PixelType; +import org.joml.Vector2i; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +public class RenderTarget { + @Getter + private final InternalTextureFormat internalFormat; + private final PixelFormat format; + private final PixelType type; + @Getter + private int width; + @Getter + private int height; + + private boolean isValid; + private final int mainTexture; + private final int altTexture; + + private static final ByteBuffer NULL_BUFFER = null; + + public RenderTarget(Builder builder) { + this.isValid = true; + + this.internalFormat = builder.internalFormat; + this.format = builder.format; + this.type = builder.type; + + this.width = builder.width; + this.height = builder.height; + + IntBuffer textures = BufferUtils.createIntBuffer(2); + GL11.glGenTextures(textures); + + this.mainTexture = textures.get(0); + this.altTexture = textures.get(1); + + boolean isPixelFormatInteger = builder.internalFormat.getPixelFormat().isInteger(); + setupTexture(mainTexture, builder.width, builder.height, !isPixelFormatInteger); + setupTexture(altTexture, builder.width, builder.height, !isPixelFormatInteger); + + // Clean up after ourselves + // This is strictly defensive to ensure that other buggy code doesn't tamper with our textures + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + } + + private void setupTexture(int texture, int width, int height, boolean allowsLinear) { + resizeTexture(texture, width, height); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, allowsLinear ? GL11.GL_LINEAR : GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, allowsLinear ? GL11.GL_LINEAR : GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + } + + private void resizeTexture(int texture, int width, int height) { + IrisRenderSystem.texImage2D(texture, GL11.GL_TEXTURE_2D, 0, internalFormat.getGlFormat(), width, height, 0, format.getGlFormat(), type.getGlFormat(), NULL_BUFFER); + } + + void resize(Vector2i textureScaleOverride) { + this.resize(textureScaleOverride.x, textureScaleOverride.y); + } + + // Package private, call CompositeRenderTargets#resizeIfNeeded instead. + void resize(int width, int height) { + requireValid(); + + this.width = width; + this.height = height; + + resizeTexture(mainTexture, width, height); + + resizeTexture(altTexture, width, height); + } + + public int getMainTexture() { + requireValid(); + + return mainTexture; + } + + public int getAltTexture() { + requireValid(); + + return altTexture; + } + + private final IntBuffer deleteBuffer = BufferUtils.createIntBuffer(2); + public void destroy() { + requireValid(); + isValid = false; + deleteBuffer.put(0, mainTexture); + deleteBuffer.put(1, altTexture); + GLStateManager.glDeleteTextures(deleteBuffer); + } + + private void requireValid() { + if (!isValid) { + throw new IllegalStateException("Attempted to use a deleted composite render target"); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private InternalTextureFormat internalFormat = InternalTextureFormat.RGBA8; + private int width = 0; + private int height = 0; + private PixelFormat format = PixelFormat.RGBA; + private PixelType type = PixelType.UNSIGNED_BYTE; + + private Builder() { + // No-op + } + + public Builder setInternalFormat(InternalTextureFormat format) { + this.internalFormat = format; + + return this; + } + + public Builder setDimensions(int width, int height) { + if (width <= 0) { + throw new IllegalArgumentException("Width must be greater than zero"); + } + + if (height <= 0) { + throw new IllegalArgumentException("Height must be greater than zero"); + } + + this.width = width; + this.height = height; + + return this; + } + + public Builder setPixelFormat(PixelFormat pixelFormat) { + this.format = pixelFormat; + + return this; + } + + public Builder setPixelType(PixelType pixelType) { + this.type = pixelType; + + return this; + } + + public RenderTarget build() { + return new RenderTarget(this); + } + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/RenderTargets.java b/src/main/java/net/coderbot/iris/rendertarget/RenderTargets.java new file mode 100644 index 000000000..38fad1c26 --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/RenderTargets.java @@ -0,0 +1,325 @@ +package net.coderbot.iris.rendertarget; + +import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import lombok.Getter; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.texture.DepthBufferFormat; +import net.coderbot.iris.gl.texture.DepthCopyStrategy; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import org.joml.Vector2i; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RenderTargets { + private final RenderTarget[] targets; + private int currentDepthTexture; + private DepthBufferFormat currentDepthFormat; + + @Getter + private final DepthTexture noTranslucents; + private final DepthTexture noHand; + private final GlFramebuffer depthSourceFb; + private final GlFramebuffer noTranslucentsDestFb; + private final GlFramebuffer noHandDestFb; + private DepthCopyStrategy copyStrategy; + + private final List ownedFramebuffers; + + private int cachedWidth; + private int cachedHeight; + @Getter + private boolean fullClearRequired; + private boolean translucentDepthDirty; + private boolean handDepthDirty; + + private int cachedDepthBufferVersion; + + public RenderTargets(int width, int height, int depthTexture, int depthBufferVersion, DepthBufferFormat depthFormat, Map renderTargets, PackDirectives packDirectives) { + targets = new RenderTarget[renderTargets.size()]; + + renderTargets.forEach((index, settings) -> { + // TODO: Handle mipmapping? + Vector2i dimensions = packDirectives.getTextureScaleOverride(index, width, height); + targets[index] = RenderTarget.builder().setDimensions(dimensions.x, dimensions.y) + .setInternalFormat(settings.getInternalFormat()) + .setPixelFormat(settings.getInternalFormat().getPixelFormat()).build(); + }); + this.currentDepthTexture = depthTexture; + this.currentDepthFormat = depthFormat; + this.copyStrategy = DepthCopyStrategy.fastest(currentDepthFormat.isCombinedStencil()); + + this.cachedWidth = width; + this.cachedHeight = height; + this.cachedDepthBufferVersion = depthBufferVersion; + + this.ownedFramebuffers = new ArrayList<>(); + + // NB: Make sure all buffers are cleared so that they don't contain undefined + // data. Otherwise very weird things can happen. + fullClearRequired = true; + + this.depthSourceFb = createFramebufferWritingToMain(new int[] {0}); + + this.noTranslucents = new DepthTexture(width, height, currentDepthFormat); + this.noHand = new DepthTexture(width, height, currentDepthFormat); + + this.noTranslucentsDestFb = createFramebufferWritingToMain(new int[] {0}); + this.noTranslucentsDestFb.addDepthAttachment(this.noTranslucents.getTextureId()); + + this.noHandDestFb = createFramebufferWritingToMain(new int[] {0}); + this.noHandDestFb.addDepthAttachment(this.noHand.getTextureId()); + + this.translucentDepthDirty = true; + this.handDepthDirty = true; + } + + public void destroy() { + for (GlFramebuffer owned : ownedFramebuffers) { + owned.destroy(); + } + + for (RenderTarget target : targets) { + target.destroy(); + } + + noTranslucents.destroy(); + noHand.destroy(); + } + + public int getRenderTargetCount() { + return targets.length; + } + + public RenderTarget get(int index) { + return targets[index]; + } + + public int getDepthTexture() { + return currentDepthTexture; + } + + public DepthTexture getDepthTextureNoTranslucents() { + return noTranslucents; + } + + public DepthTexture getDepthTextureNoHand() { + return noHand; + } + + public boolean resizeIfNeeded(int newDepthBufferVersion, int newDepthTextureId, int newWidth, int newHeight, DepthBufferFormat newDepthFormat, PackDirectives packDirectives) { + boolean recreateDepth = false; + if (cachedDepthBufferVersion != newDepthBufferVersion) { + recreateDepth = true; + currentDepthTexture = newDepthTextureId; + cachedDepthBufferVersion = newDepthBufferVersion; + } + + boolean sizeChanged = newWidth != cachedWidth || newHeight != cachedHeight; + boolean depthFormatChanged = newDepthFormat != currentDepthFormat; + + if (depthFormatChanged) { + currentDepthFormat = newDepthFormat; + // Might need a new copy strategy + copyStrategy = DepthCopyStrategy.fastest(currentDepthFormat.isCombinedStencil()); + } + + if (recreateDepth) { + // Re-attach the depth textures with the new depth texture ID, since Minecraft re-creates + // the depth texture when resizing its render targets. + // + // I'm not sure if our framebuffers holding on to the old depth texture between frames + // could be a concern, in the case of resizing and similar. I think it should work + // based on what I've seen of the spec, though - it seems like deleting a texture + // automatically detaches it from its framebuffers. + for (GlFramebuffer framebuffer : ownedFramebuffers) { + if (framebuffer == noHandDestFb || framebuffer == noTranslucentsDestFb) { + // NB: Do not change the depth attachment of these framebuffers + // as it is intentionally different + continue; + } + + if (framebuffer.hasDepthAttachment()) { + framebuffer.addDepthAttachment(newDepthTextureId); + } + } + } + + if (depthFormatChanged || sizeChanged) { + // Reallocate depth buffers + noTranslucents.resize(newWidth, newHeight, newDepthFormat); + noHand.resize(newWidth, newHeight, newDepthFormat); + this.translucentDepthDirty = true; + this.handDepthDirty = true; + } + + if (sizeChanged) { + cachedWidth = newWidth; + cachedHeight = newHeight; + + for (int i = 0; i < targets.length; i++) { + targets[i].resize(packDirectives.getTextureScaleOverride(i, newWidth, newHeight)); + } + + fullClearRequired = true; + } + + return sizeChanged; + } + + public void copyPreTranslucentDepth() { + if (translucentDepthDirty) { + translucentDepthDirty = false; + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, noTranslucents.getTextureId()); + depthSourceFb.bindAsReadBuffer(); + IrisRenderSystem.copyTexImage2D(GL11.GL_TEXTURE_2D, 0, currentDepthFormat.getGlInternalFormat(), 0, 0, cachedWidth, cachedHeight, 0); + } else { + copyStrategy.copy(depthSourceFb, getDepthTexture(), noTranslucentsDestFb, noTranslucents.getTextureId(), getCurrentWidth(), getCurrentHeight()); + } + } + + public void copyPreHandDepth() { + if (handDepthDirty) { + handDepthDirty = false; + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, noHand.getTextureId()); + depthSourceFb.bindAsReadBuffer(); + IrisRenderSystem.copyTexImage2D(GL11.GL_TEXTURE_2D, 0, currentDepthFormat.getGlInternalFormat(), 0, 0, cachedWidth, cachedHeight, 0); + } else { + copyStrategy.copy(depthSourceFb, getDepthTexture(), noHandDestFb, noHand.getTextureId(), getCurrentWidth(), getCurrentHeight()); + } + } + + public void onFullClear() { + fullClearRequired = false; + } + + public GlFramebuffer createFramebufferWritingToMain(int[] drawBuffers) { + return createFullFramebuffer(false, drawBuffers); + } + + public GlFramebuffer createFramebufferWritingToAlt(int[] drawBuffers) { + return createFullFramebuffer(true, drawBuffers); + } + + public GlFramebuffer createClearFramebuffer(boolean alt, int[] clearBuffers) { + ImmutableSet stageWritesToMain = ImmutableSet.of(); + + if (!alt) { + stageWritesToMain = invert(ImmutableSet.of(), clearBuffers); + } + + return createColorFramebuffer(stageWritesToMain, clearBuffers); + } + + private ImmutableSet invert(ImmutableSet base, int[] relevant) { + ImmutableSet.Builder inverted = ImmutableSet.builder(); + + for (int i : relevant) { + if (!base.contains(i)) { + inverted.add(i); + } + } + + return inverted.build(); + } + + private GlFramebuffer createEmptyFramebuffer() { + GlFramebuffer framebuffer = new GlFramebuffer(); + ownedFramebuffers.add(framebuffer); + + framebuffer.addDepthAttachment(currentDepthTexture); + + // NB: Before OpenGL 3.0, all framebuffers are required to have a color attachment no matter what. + framebuffer.addColorAttachment(0, get(0).getMainTexture()); + framebuffer.noDrawBuffers(); + + return framebuffer; + } + + public GlFramebuffer createGbufferFramebuffer(ImmutableSet stageWritesToAlt, int[] drawBuffers) { + if (drawBuffers.length == 0) { + return createEmptyFramebuffer(); + } + + ImmutableSet stageWritesToMain = invert(stageWritesToAlt, drawBuffers); + GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); + framebuffer.addDepthAttachment(currentDepthTexture); + + return framebuffer; + } + + private GlFramebuffer createFullFramebuffer(boolean clearsAlt, int[] drawBuffers) { + if (drawBuffers.length == 0) { + return createEmptyFramebuffer(); + } + + ImmutableSet stageWritesToMain = ImmutableSet.of(); + + if (!clearsAlt) { + stageWritesToMain = invert(ImmutableSet.of(), drawBuffers); + } + + return createColorFramebufferWithDepth(stageWritesToMain, drawBuffers); + } + + public GlFramebuffer createColorFramebufferWithDepth(ImmutableSet stageWritesToMain, int[] drawBuffers) { + final GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); + framebuffer.addDepthAttachment(currentDepthTexture); + + return framebuffer; + } + + public GlFramebuffer createColorFramebuffer(ImmutableSet stageWritesToMain, int[] drawBuffers) { + if (drawBuffers.length == 0) { + throw new IllegalArgumentException("Framebuffer must have at least one color buffer"); + } + + final GlFramebuffer framebuffer = new GlFramebuffer(); + ownedFramebuffers.add(framebuffer); + + final int[] actualDrawBuffers = new int[drawBuffers.length]; + + for (int i = 0; i < drawBuffers.length; i++) { + actualDrawBuffers[i] = i; + + if (drawBuffers[i] >= getRenderTargetCount()) { + // TODO: This causes resource leaks, also we should really verify this in the shaderpack parser... + throw new IllegalStateException("Render target with index " + drawBuffers[i] + " is not supported, only " + + getRenderTargetCount() + " render targets are supported."); + } + + final RenderTarget target = this.get(drawBuffers[i]); + + final int textureId = stageWritesToMain.contains(drawBuffers[i]) ? target.getMainTexture() : target.getAltTexture(); + + framebuffer.addColorAttachment(i, textureId); + } + + framebuffer.drawBuffers(actualDrawBuffers); + framebuffer.readBuffer(0); + + if (!framebuffer.isComplete()) { + throw new IllegalStateException("Unexpected error while creating framebuffer"); + } + + return framebuffer; + } + + public void destroyFramebuffer(GlFramebuffer framebuffer) { + framebuffer.destroy(); + ownedFramebuffers.remove(framebuffer); + } + + public int getCurrentWidth() { + return cachedWidth; + } + + public int getCurrentHeight() { + return cachedHeight; + } +} diff --git a/src/main/java/net/coderbot/iris/rendertarget/SingleColorTexture.java b/src/main/java/net/coderbot/iris/rendertarget/SingleColorTexture.java new file mode 100644 index 000000000..b8d30c20d --- /dev/null +++ b/src/main/java/net/coderbot/iris/rendertarget/SingleColorTexture.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.rendertarget; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.GlResource; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.texture.TextureUploadHelper; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; + +public class SingleColorTexture extends GlResource { + public SingleColorTexture(int red, int green, int blue, int alpha) { + super(IrisRenderSystem.createTexture(GL11.GL_TEXTURE_2D)); + ByteBuffer pixel = BufferUtils.createByteBuffer(4); + pixel.put((byte) red); + pixel.put((byte) green); + pixel.put((byte) blue); + pixel.put((byte) alpha); + pixel.position(0); + + int texture = getGlId(); + + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); + IrisRenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); + + TextureUploadHelper.resetTextureUploadState(); + IrisRenderSystem.texImage2D(texture, GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel); + } + + public int getTextureId() { + return getGlId(); + } + + @Override + protected void destroyInternal() { + GLStateManager.glDeleteTextures(getGlId()); + } +} diff --git a/src/main/java/net/coderbot/iris/samplers/IrisImages.java b/src/main/java/net/coderbot/iris/samplers/IrisImages.java new file mode 100644 index 000000000..7ed7e9977 --- /dev/null +++ b/src/main/java/net/coderbot/iris/samplers/IrisImages.java @@ -0,0 +1,53 @@ +package net.coderbot.iris.samplers; + +import com.google.common.collect.ImmutableSet; +import net.coderbot.iris.gl.image.ImageHolder; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.shadows.ShadowRenderTargets; + +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class IrisImages { + public static void addRenderTargetImages(ImageHolder images, Supplier> flipped, + RenderTargets renderTargets) { + for (int i = 0; i < renderTargets.getRenderTargetCount(); i++) { + final int index = i; + + // Note: image bindings *are* impacted by buffer flips. + IntSupplier textureID = () -> { + ImmutableSet flippedBuffers = flipped.get(); + RenderTarget target = renderTargets.get(index); + + if (flippedBuffers.contains(index)) { + return target.getAltTexture(); + } else { + return target.getMainTexture(); + } + }; + + final InternalTextureFormat internalFormat = renderTargets.get(i).getInternalFormat(); + final String name = "colorimg" + i; + + images.addTextureImage(textureID, internalFormat, name); + } + } + + public static boolean hasShadowImages(ImageHolder images) { + // TODO: Generalize + return images.hasImage("shadowcolorimg0") || images.hasImage("shadowcolorimg1"); + } + + public static void addShadowColorImages(ImageHolder images, ShadowRenderTargets shadowRenderTargets) { + for (int i = 0; i < shadowRenderTargets.getNumColorTextures(); i++) { + final int index = i; + + IntSupplier textureID = () -> shadowRenderTargets.getColorTextureId(index); + InternalTextureFormat format = shadowRenderTargets.getColorTextureFormat(index); + + images.addTextureImage(textureID, format, "shadowcolorimg" + i); + } + } +} diff --git a/src/main/java/net/coderbot/iris/samplers/IrisSamplers.java b/src/main/java/net/coderbot/iris/samplers/IrisSamplers.java new file mode 100644 index 000000000..38c8d4303 --- /dev/null +++ b/src/main/java/net/coderbot/iris/samplers/IrisSamplers.java @@ -0,0 +1,167 @@ +package net.coderbot.iris.samplers; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.gl.sampler.SamplerHolder; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.rendertarget.RenderTargets; +import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; +import net.coderbot.iris.shadows.ShadowRenderTargets; +import net.minecraft.client.renderer.texture.AbstractTexture; + +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class IrisSamplers { + public static final int ALBEDO_TEXTURE_UNIT = 0; + // TODO: Find equivalent in 1.7.10 + public static final int OVERLAY_TEXTURE_UNIT = 1; + public static final int LIGHTMAP_TEXTURE_UNIT = 1; + + public static final ImmutableSet WORLD_RESERVED_TEXTURE_UNITS = ImmutableSet.of(0, 1, 2); + + // TODO: In composite programs, there shouldn't be any reserved textures. + // We need a way to restore these texture bindings. + public static final ImmutableSet COMPOSITE_RESERVED_TEXTURE_UNITS = ImmutableSet.of(1, 2); + + private IrisSamplers() { + // no construction allowed + } + + public static void addRenderTargetSamplers(SamplerHolder samplers, Supplier> flipped, + RenderTargets renderTargets, boolean isFullscreenPass) { + // colortex0,1,2,3 are only able to be sampled from fullscreen passes. + // Iris could lift this restriction, though I'm not sure if it could cause issues. + int startIndex = isFullscreenPass ? 0 : 4; + + for (int i = startIndex; i < renderTargets.getRenderTargetCount(); i++) { + final int index = i; + + IntSupplier sampler = () -> { + ImmutableSet flippedBuffers = flipped.get(); + RenderTarget target = renderTargets.get(index); + + if (flippedBuffers.contains(index)) { + return target.getAltTexture(); + } else { + return target.getMainTexture(); + } + }; + + final String name = "colortex" + i; + + // TODO: How do custom textures interact with aliases? + + if (i < PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.size()) { + String legacyName = PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.get(i); + + // colortex0 is the default sampler in fullscreen passes + if (i == 0 && isFullscreenPass) { + samplers.addDefaultSampler(sampler, name, legacyName); + } else { + samplers.addDynamicSampler(sampler, name, legacyName); + } + } else { + samplers.addDynamicSampler(sampler, name); + } + } + } + + public static void addNoiseSampler(SamplerHolder samplers, IntSupplier sampler) { + samplers.addDynamicSampler(sampler, "noisetex"); + } + + public static boolean hasShadowSamplers(SamplerHolder samplers) { + // TODO: Keep this up to date with the actual definitions. + // TODO: Don't query image presence using the sampler interface even though the current underlying implementation + // is the same. + ImmutableList shadowSamplers = ImmutableList.of("shadowtex0", "shadowtex0HW", "shadowtex1", "shadowtex1HW", "shadow", "watershadow", + "shadowcolor", "shadowcolor0", "shadowcolor1", "shadowcolorimg0", "shadowcolorimg1"); + + for (String samplerName : shadowSamplers) { + if (samplers.hasSampler(samplerName)) { + return true; + } + } + + return false; + } + + public static boolean addShadowSamplers(SamplerHolder samplers, ShadowRenderTargets shadowRenderTargets) { + boolean usesShadows; + + // TODO: figure this out from parsing the shader source code to be 100% compatible with the legacy + // shader packs that rely on this behavior. + boolean waterShadowEnabled = samplers.hasSampler("watershadow"); + + if (waterShadowEnabled) { + usesShadows = true; + samplers.addDynamicSampler(shadowRenderTargets.getDepthTexture()::getTextureId, "shadowtex0", "watershadow"); + samplers.addDynamicSampler(shadowRenderTargets.getDepthTextureNoTranslucents()::getTextureId, "shadowtex1", "shadow"); + } else { + usesShadows = samplers.addDynamicSampler(shadowRenderTargets.getDepthTexture()::getTextureId, "shadowtex0", "shadow"); + usesShadows |= samplers.addDynamicSampler(shadowRenderTargets.getDepthTextureNoTranslucents()::getTextureId, "shadowtex1"); + } + + samplers.addDynamicSampler(() -> shadowRenderTargets.getColorTextureId(0), "shadowcolor", "shadowcolor0"); + samplers.addDynamicSampler(() -> shadowRenderTargets.getColorTextureId(1), "shadowcolor1"); + + if (shadowRenderTargets.isHardwareFiltered(0)) { + samplers.addDynamicSampler(shadowRenderTargets.getDepthTexture()::getTextureId, "shadowtex0HW"); + } + + if (shadowRenderTargets.isHardwareFiltered(1)) { + samplers.addDynamicSampler(shadowRenderTargets.getDepthTextureNoTranslucents()::getTextureId, "shadowtex1HW"); + } + + return usesShadows; + } + + public static boolean hasPBRSamplers(SamplerHolder samplers) { + return samplers.hasSampler("normals") || samplers.hasSampler("specular"); + } + + public static void addLevelSamplers(SamplerHolder samplers, WorldRenderingPipeline pipeline, AbstractTexture whitePixel, InputAvailability availability) { + if (availability.texture) { + samplers.addExternalSampler(ALBEDO_TEXTURE_UNIT, "tex", "texture", "gtexture"); + } else { + // TODO: Rebind unbound sampler IDs instead of hardcoding a list... + samplers.addDynamicSampler(whitePixel::getGlTextureId, "tex", "texture", "gtexture", "gcolor", "colortex0"); + } + + if (availability.lightmap) { + samplers.addExternalSampler(LIGHTMAP_TEXTURE_UNIT, "lightmap"); + } else { + samplers.addDynamicSampler(whitePixel::getGlTextureId, "lightmap"); + } + + if (availability.overlay) { + // TODO: Overlay equivalent in 1.7.10? +// samplers.addExternalSampler(OVERLAY_TEXTURE_UNIT, "iris_overlay"); + samplers.addDynamicSampler(whitePixel::getGlTextureId, "iris_overlay"); + } else { + samplers.addDynamicSampler(whitePixel::getGlTextureId, "iris_overlay"); + } + + samplers.addDynamicSampler(pipeline::getCurrentNormalTexture, StateUpdateNotifiers.normalTextureChangeNotifier, "normals"); + samplers.addDynamicSampler(pipeline::getCurrentSpecularTexture, StateUpdateNotifiers.specularTextureChangeNotifier, "specular"); + } + + public static void addWorldDepthSamplers(SamplerHolder samplers, RenderTargets renderTargets) { + samplers.addDynamicSampler(renderTargets::getDepthTexture, "depthtex0"); + // TODO: Should depthtex2 be made available to gbuffer / shadow programs? + samplers.addDynamicSampler(renderTargets.getDepthTextureNoTranslucents()::getTextureId, "depthtex1"); + } + + public static void addCompositeSamplers(SamplerHolder samplers, RenderTargets renderTargets) { + samplers.addDynamicSampler(renderTargets::getDepthTexture, + "gdepthtex", "depthtex0"); + samplers.addDynamicSampler(renderTargets.getDepthTextureNoTranslucents()::getTextureId, + "depthtex1"); + samplers.addDynamicSampler(renderTargets.getDepthTextureNoHand()::getTextureId, + "depthtex2"); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/CloudSetting.java b/src/main/java/net/coderbot/iris/shaderpack/CloudSetting.java new file mode 100644 index 000000000..18a69f6f2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/CloudSetting.java @@ -0,0 +1,8 @@ +package net.coderbot.iris.shaderpack; + +public enum CloudSetting { + DEFAULT, + FAST, + FANCY, + OFF +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/CommentDirective.java b/src/main/java/net/coderbot/iris/shaderpack/CommentDirective.java new file mode 100644 index 000000000..8d64506c2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/CommentDirective.java @@ -0,0 +1,38 @@ +package net.coderbot.iris.shaderpack; + +public class CommentDirective { + public enum Type { + DRAWBUFFERS, + RENDERTARGETS + } + + private final Type type; + private final String directive; + private final int location; + + CommentDirective(Type type, String directive, int location) { + this.type = type; + this.directive = directive; + this.location = location; + } + + public Type getType() { + return type; + } + + /** + * @return The directive without {@literal /}* or *{@literal /} + */ + public String getDirective() { + return directive; + } + + /** + * @return The starting position of the directive in a multi-line string.
+ * This is necessary to check if either the drawbuffer or the rendertarget directive should be applied + * when there are multiple in the same shader file, based on which one is defined last. + */ + public int getLocation() { + return location; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/CommentDirectiveParser.java b/src/main/java/net/coderbot/iris/shaderpack/CommentDirectiveParser.java new file mode 100644 index 000000000..88808c61f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/CommentDirectiveParser.java @@ -0,0 +1,151 @@ +package net.coderbot.iris.shaderpack; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Parses comment-based directives found in shader source files of the form: + * + *
/* KEY:VALUE */
+ *

+ * A common example is draw buffer directives: + * + *

/* DRAWBUFFERS:157 */
+ *

+ * A given directive should only occur once in a shader file. If there are multiple occurrences of a directive with a + * given key, the last occurrence is used. + */ +public class CommentDirectiveParser { + // Directives take the following form: + // /* KEY:VALUE */ + + private CommentDirectiveParser() { + // cannot be constructed + } + + public static Optional findDirective(String haystack, CommentDirective.Type type) { + String needle = type.name(); + String prefix = needle + ":"; + String suffix = "*/"; + + // Search for the last occurrence of the directive within the text, since those take precedence. + int indexOfPrefix = haystack.lastIndexOf(prefix); + + if (indexOfPrefix == -1) { + return Optional.empty(); + } + + String before = haystack.substring(0, indexOfPrefix).trim(); + + if (!before.endsWith("/*")) { + // Reject a match if it doesn't actually start with a comment marker + // TODO: If a line has two directives, one valid, and the other invalid, then this might not work properly + return Optional.empty(); + } + + // Remove everything up to and including the prefix + haystack = haystack.substring(indexOfPrefix + prefix.length()); + + int indexOfSuffix = haystack.indexOf(suffix); + + // If there isn't a proper suffix, this directive is malformed and should be discarded. + if (indexOfSuffix == -1) { + return Optional.empty(); + } + + // Remove the suffix and everything afterwards, also trim any whitespace + haystack = haystack.substring(0, indexOfSuffix).trim(); + + return Optional.of(new CommentDirective(CommentDirective.Type.valueOf(needle), haystack, indexOfPrefix)); + } + + // Test code for directive parsing. It's a bit homegrown but it works. + @SuppressWarnings("unused") + private static class Tests { + private static void test(String name, T expected, Supplier testCase) { + T actual; + + try { + actual = testCase.get(); + } catch (Throwable e) { + System.err.println("Test \"" + name + "\" failed with an exception:"); + e.printStackTrace(); + + return; + } + + if (!expected.equals(actual)) { + System.err.println("Test \"" + name + "\" failed: Expected " + expected + ", got " + actual); + } else { + System.out.println("Test \"" + name + "\" passed"); + } + } + + public static void main(String[] args) { + test("normal text", Optional.empty(), () -> { + String line = "Some normal text that doesn't contain a DRAWBUFFERS directive of any sort"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("partial directive", Optional.empty(), () -> { + String line = "Some normal text that doesn't contain a /* DRAWBUFFERS: directive of any sort"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("bad spacing", Optional.of("321"), () -> { + String line = "/*DRAWBUFFERS:321*/ OptiFine will detect this directive, but ShadersMod will not..."; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("matchAtEnd", Optional.of("321"), () -> { + String line = "A line containing a drawbuffers directive: /* DRAWBUFFERS:321 */"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("matchAtStart", Optional.of("31"), () -> { + String line = "/* DRAWBUFFERS:31 */ This is a line containing a drawbuffers directive"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("matchInMiddle", Optional.of("31"), () -> { + String line = "This is a line /* DRAWBUFFERS:31 */ containing a drawbuffers directive"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("emptyMatch", Optional.of(""), () -> { + String line = "/* DRAWBUFFERS: */ This is a line containing an invalid but still matching drawbuffers directive"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("duplicates", Optional.of("3"), () -> { + String line = "/* TEST:2 */ This line contains multiple directives, the last one should be used /* TEST:3 */"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + + test("multi-line", Optional.of("It works"), () -> { + String lines = + "/* Here's a random comment line */\n" + + "/* RENDERTARGETS:Duplicate handling? */\n" + + "uniform sampler2D test;\n" + + "/* RENDERTARGETS:Duplicate handling within a line? */ Let's see /* RENDERTARGETS:It works */\n"; + + return CommentDirectiveParser.findDirective(lines, CommentDirective.Type.RENDERTARGETS).map(CommentDirective::getDirective); + }); + + // OptiFine finds this directive, but ShadersMod does not... + test("bad spacing from BSL composite6", Optional.of("12"), () -> { + String line = " /*DRAWBUFFERS:12*/"; + + return CommentDirectiveParser.findDirective(line, CommentDirective.Type.DRAWBUFFERS).map(CommentDirective::getDirective); + }); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ComputeDirectiveParser.java b/src/main/java/net/coderbot/iris/shaderpack/ComputeDirectiveParser.java new file mode 100644 index 000000000..8ef4e266b --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ComputeDirectiveParser.java @@ -0,0 +1,75 @@ +package net.coderbot.iris.shaderpack; + +import net.coderbot.iris.Iris; +import org.joml.Vector2f; +import org.joml.Vector3i; + +public class ComputeDirectiveParser { + public static void setComputeWorkGroups(ComputeSource source, ConstDirectiveParser.ConstDirective directive) { + if (!directive.getValue().startsWith("ivec3")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid ivec3 constructor"); + } + + String ivec3Args = directive.getValue().substring("ivec3".length()).trim(); + + if (!ivec3Args.startsWith("(") || !ivec3Args.endsWith(")")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid ivec3 constructor"); + } + + ivec3Args = ivec3Args.substring(1, ivec3Args.length() - 1); + + String[] parts = ivec3Args.split(","); + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + } + + if (parts.length != 3) { + Iris.logger.error("Failed to process " + directive + + ": expected 3 arguments to a ivec3 constructor, got " + parts.length); + } + + try { + source.setWorkGroups(new Vector3i( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2]))); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + } + + public static void setComputeWorkGroupsRelative(ComputeSource source, ConstDirectiveParser.ConstDirective directive) { + if (!directive.getValue().startsWith("vec2")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec2 constructor"); + } + + String vec2Args = directive.getValue().substring("vec2".length()).trim(); + + if (!vec2Args.startsWith("(") || !vec2Args.endsWith(")")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec2 constructor"); + } + + vec2Args = vec2Args.substring(1, vec2Args.length() - 1); + + String[] parts = vec2Args.split(","); + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + } + + if (parts.length != 2) { + Iris.logger.error("Failed to process " + directive + + ": expected 2 arguments to a vec2 constructor, got " + parts.length); + } + + try { + source.setWorkGroupRelative(new Vector2f( + Float.parseFloat(parts[0]), + Float.parseFloat(parts[1]) + )); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ComputeSource.java b/src/main/java/net/coderbot/iris/shaderpack/ComputeSource.java new file mode 100644 index 000000000..c0dff55ed --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ComputeSource.java @@ -0,0 +1,60 @@ +package net.coderbot.iris.shaderpack; + +import org.joml.Vector2f; +import org.joml.Vector3i; + +import java.util.Optional; + +public class ComputeSource { + private final String name; + private final String source; + private final ProgramSet parent; + private Vector3i workGroups; + private Vector2f workGroupRelative; + + public ComputeSource(String name, String source, ProgramSet parent) { + this.name = name; + this.source = source; + this.parent = parent; + } + + public String getName() { + return name; + } + + public Optional getSource() { + return Optional.ofNullable(source); + } + + public ProgramSet getParent() { + return parent; + } + + public boolean isValid() { + return source != null; + } + + public void setWorkGroups(Vector3i workGroups) { + this.workGroups = workGroups; + } + + public void setWorkGroupRelative(Vector2f workGroupRelative) { + this.workGroupRelative = workGroupRelative; + } + + public Vector2f getWorkGroupRelative() { + return workGroupRelative; + } + + public Vector3i getWorkGroups() { + return workGroups; + } + + public Optional requireValid() { + if (this.isValid()) { + return Optional.of(this); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ConstDirectiveParser.java b/src/main/java/net/coderbot/iris/shaderpack/ConstDirectiveParser.java new file mode 100644 index 000000000..f8d80d232 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ConstDirectiveParser.java @@ -0,0 +1,178 @@ +package net.coderbot.iris.shaderpack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class ConstDirectiveParser { + public static List findDirectives(String source) { + List directives = new ArrayList<>(); + + // Match any valid newline sequence + // https://stackoverflow.com/a/31060125 + for (String line : source.split("\\R")) { + findDirectiveInLine(line).ifPresent(directives::add); + } + + return directives; + } + + public static Optional findDirectiveInLine(String line) { + // Valid const directives contain the following elements: + // * Zero or more whitespace characters + // * A "const" literal + // * At least one whitespace character + // * A type literal (int, float, vec4, or bool) + // * At least one whitespace character + // * The name / key of the const directive (alphanumeric & underscore characters) + // * Zero or more whitespace characters + // * An equals sign + // * Zero or more whitespace characters + // * The value of the const directive (alphanumeric & underscore characters) + // * A semicolon + // * (any content) + + // Bail-out early without doing any processing if required components are not found + // A const directive must contain at the very least a const keyword, then an equals + // sign, then a semicolon. + if (!line.contains("const") || !line.contains("=") || !line.contains(";")) { + return Optional.empty(); + } + + // Trim any surrounding whitespace (such as indentation) from the line before processing it. + line = line.trim(); + + // A valid declaration must have a trimmed line starting with const + if (!line.startsWith("const")) { + return Optional.empty(); + } + + // Remove the const part from the string + line = line.substring("const".length()); + + // There must be at least one whitespace character between the "const" keyword and the type keyword + if (!startsWithWhitespace(line)) { + return Optional.empty(); + } + + // Trim all whitespace between the const keyword and the type keyword + line = line.trim(); + + // Valid const declarations have a type that is either an int, a float, a vec4, or a bool. + Type type; + + if (line.startsWith("int")) { + type = Type.INT; + line = line.substring("int".length()); + } else if (line.startsWith("float")) { + type = Type.FLOAT; + line = line.substring("float".length()); + } else if (line.startsWith("vec2")) { + type = Type.VEC2; + line = line.substring("vec2".length()); + } else if (line.startsWith("ivec3")) { + type = Type.IVEC3; + line = line.substring("ivec3".length()); + } else if (line.startsWith("vec4")) { + type = Type.VEC4; + line = line.substring("vec4".length()); + } else if (line.startsWith("bool")) { + type = Type.BOOL; + line = line.substring("bool".length()); + } else { + return Optional.empty(); + } + + // There must be at least one whitespace character between the type keyword and the key of the const declaration + if (!startsWithWhitespace(line)) { + return Optional.empty(); + } + + // Split the declaration at the equals sign + int equalsIndex = line.indexOf('='); + + if (equalsIndex == -1) { + // No equals sign found, not a valid const declaration + return Optional.empty(); + } + + // The key comes before the equals sign + String key = line.substring(0, equalsIndex).trim(); + + // The key must be a "word" (alphanumeric & underscore characters) + if (!isWord(key)) { + return Optional.empty(); + } + + // Everything after the equals sign but before the semicolon is the value + String remaining = line.substring(equalsIndex + 1); + + int semicolonIndex = remaining.indexOf(';'); + + if (semicolonIndex == -1) { + // No semicolon found, not a valid const declaration + return Optional.empty(); + } + + String value = remaining.substring(0, semicolonIndex).trim(); + + // We make no attempt to properly parse / verify the value here, that responsibility lies with whatever code + // is working with the directives. + return Optional.of(new ConstDirective(type, key, value)); + } + + private static boolean startsWithWhitespace(String text) { + return !text.isEmpty() && Character.isWhitespace(text.charAt(0)); + } + + private static boolean isWord(String text) { + if (text.isEmpty()) { + return false; + } + + for (char character : text.toCharArray()) { + if (!Character.isDigit(character) && !Character.isAlphabetic(character) && character != '_') { + return false; + } + } + + return true; + } + + public static class ConstDirective { + private final Type type; + private final String key; + private final String value; + + ConstDirective(Type type, String key, String value) { + this.type = type; + this.key = key; + this.value = value; + } + + public Type getType() { + return type; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String toString() { + return "ConstDirective { " + type + " " + key + " = " + value + "; }"; + } + } + + public enum Type { + INT, + FLOAT, + VEC2, + IVEC3, + VEC4, + BOOL + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/DimensionId.java b/src/main/java/net/coderbot/iris/shaderpack/DimensionId.java new file mode 100644 index 000000000..8254ca810 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/DimensionId.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.shaderpack; + +public enum DimensionId { + OVERWORLD, + NETHER, + END +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/DirectiveHolder.java b/src/main/java/net/coderbot/iris/shaderpack/DirectiveHolder.java new file mode 100644 index 000000000..f821d8df1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/DirectiveHolder.java @@ -0,0 +1,24 @@ +package net.coderbot.iris.shaderpack; + +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import it.unimi.dsi.fastutil.floats.FloatConsumer; +import org.joml.Vector2f; +import org.joml.Vector3i; +import org.joml.Vector4f; + +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public interface DirectiveHolder { + void acceptUniformDirective(String name, Runnable onDetected); + void acceptCommentStringDirective(String name, Consumer consumer); + void acceptCommentIntDirective(String name, IntConsumer consumer); + void acceptCommentFloatDirective(String name, FloatConsumer consumer); + void acceptConstBooleanDirective(String name, BooleanConsumer consumer); + void acceptConstStringDirective(String name, Consumer consumer); + void acceptConstIntDirective(String name, IntConsumer consumer); + void acceptConstFloatDirective(String name, FloatConsumer consumer); + void acceptConstVec2Directive(String name, Consumer consumer); + void acceptConstIVec3Directive(String name, Consumer consumer); + void acceptConstVec4Directive(String name, Consumer consumer); +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/DispatchingDirectiveHolder.java b/src/main/java/net/coderbot/iris/shaderpack/DispatchingDirectiveHolder.java new file mode 100644 index 000000000..2101306d9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/DispatchingDirectiveHolder.java @@ -0,0 +1,323 @@ +package net.coderbot.iris.shaderpack; + +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import it.unimi.dsi.fastutil.floats.FloatConsumer; +import net.coderbot.iris.Iris; +import net.coderbot.iris.IrisLogging; +import org.joml.Vector2f; +import org.joml.Vector3i; +import org.joml.Vector4f; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public class DispatchingDirectiveHolder implements DirectiveHolder { + private final Map booleanConstVariables; + private final Map> stringConstVariables; + private final Map intConstVariables; + private final Map floatConstVariables; + private final Map> vec2ConstVariables; + private final Map> ivec3ConstVariables; + private final Map> vec4ConstVariables; + + public DispatchingDirectiveHolder() { + booleanConstVariables = new HashMap<>(); + stringConstVariables = new HashMap<>(); + intConstVariables = new HashMap<>(); + floatConstVariables = new HashMap<>(); + vec2ConstVariables = new HashMap<>(); + ivec3ConstVariables = new HashMap<>(); + vec4ConstVariables = new HashMap<>(); + } + + public void processDirective(ConstDirectiveParser.ConstDirective directive) { + final ConstDirectiveParser.Type type = directive.getType(); + final String key = directive.getKey(); + final String value = directive.getValue(); + + if (type == ConstDirectiveParser.Type.BOOL) { + BooleanConsumer consumer = booleanConstVariables.get(key); + + if (consumer != null) { + if ("true".equals(value)) { + consumer.accept(true); + } else if ("false".equals(value)) { + consumer.accept(false); + } else { + Iris.logger.error("Failed to process " + directive + ": " + value + " is not a valid boolean value"); + } + + return; + } + + if (IrisLogging.ENABLE_SPAM) { + // Only logspam in dev + Iris.logger.info("Found potential unhandled directive: " + directive); + } + + typeCheckHelper("int", intConstVariables, directive); + typeCheckHelper("int", stringConstVariables, directive); + typeCheckHelper("float", floatConstVariables, directive); + typeCheckHelper("vec4", vec4ConstVariables, directive); + } else if (type == ConstDirectiveParser.Type.INT) { + // GLSL does not actually have a string type, so string constant directives use "const int" instead. + Consumer stringConsumer = stringConstVariables.get(key); + + if (stringConsumer != null) { + stringConsumer.accept(value); + + return; + } + + IntConsumer intConsumer = intConstVariables.get(key); + + if (intConsumer != null) { + try { + intConsumer.accept(Integer.parseInt(value)); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + + return; + } + + if (IrisLogging.ENABLE_SPAM) { + // Only logspam in dev + Iris.logger.info("Found potential unhandled directive: " + directive); + } + + typeCheckHelper("bool", booleanConstVariables, directive); + typeCheckHelper("float", floatConstVariables, directive); + typeCheckHelper("vec4", vec4ConstVariables, directive); + } else if (type == ConstDirectiveParser.Type.FLOAT) { + FloatConsumer consumer = floatConstVariables.get(key); + + if (consumer != null) { + try { + consumer.accept(Float.parseFloat(value)); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + + return; + } + + if (IrisLogging.ENABLE_SPAM) { + // Only logspam in dev + Iris.logger.info("Found potential unhandled directive: " + directive); + } + + typeCheckHelper("bool", booleanConstVariables, directive); + typeCheckHelper("int", intConstVariables, directive); + typeCheckHelper("int", stringConstVariables, directive); + typeCheckHelper("vec4", vec4ConstVariables, directive); + } else if (type == ConstDirectiveParser.Type.VEC2) { + Consumer consumer = vec2ConstVariables.get(key); + + if (consumer != null) { + if (!value.startsWith("vec2")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec2 constructor"); + } + + String vec2Args = value.substring("vec2".length()).trim(); + + if (!vec2Args.startsWith("(") || !vec2Args.endsWith(")")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec2 constructor"); + } + + vec2Args = vec2Args.substring(1, vec2Args.length() - 1); + + String[] parts = vec2Args.split(","); + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + } + + if (parts.length != 2) { + Iris.logger.error("Failed to process " + directive + + ": expected 2 arguments to a vec2 constructor, got " + parts.length); + } + + try { + consumer.accept(new Vector2f( + Float.parseFloat(parts[0]), + Float.parseFloat(parts[1]) + )); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + + return; + } + + typeCheckHelper("bool", booleanConstVariables, directive); + typeCheckHelper("int", intConstVariables, directive); + typeCheckHelper("int", stringConstVariables, directive); + typeCheckHelper("float", floatConstVariables, directive); + } else if (type == ConstDirectiveParser.Type.IVEC3) { + Consumer consumer = ivec3ConstVariables.get(key); + + if (consumer != null) { + if (!value.startsWith("ivec3")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid ivec3 constructor"); + } + + String ivec3Args = value.substring("ivec3".length()).trim(); + + if (!ivec3Args.startsWith("(") || !ivec3Args.endsWith(")")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid ivec3 constructor"); + } + + ivec3Args = ivec3Args.substring(1, ivec3Args.length() - 1); + + String[] parts = ivec3Args.split(","); + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + } + + if (parts.length != 3) { + Iris.logger.error("Failed to process " + directive + + ": expected 3 arguments to a ivec3 constructor, got " + parts.length); + } + + try { + consumer.accept(new Vector3i( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2]) + )); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + + return; + } + + typeCheckHelper("bool", booleanConstVariables, directive); + typeCheckHelper("int", intConstVariables, directive); + typeCheckHelper("int", stringConstVariables, directive); + typeCheckHelper("float", floatConstVariables, directive); + } else if (type == ConstDirectiveParser.Type.VEC4) { + Consumer consumer = vec4ConstVariables.get(key); + + if (consumer != null) { + if (!value.startsWith("vec4")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec4 constructor"); + } + + String vec4Args = value.substring("vec4".length()).trim(); + + if (!vec4Args.startsWith("(") || !vec4Args.endsWith(")")) { + Iris.logger.error("Failed to process " + directive + ": value was not a valid vec4 constructor"); + } + + vec4Args = vec4Args.substring(1, vec4Args.length() - 1); + + String[] parts = vec4Args.split(","); + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + } + + if (parts.length != 4) { + Iris.logger.error("Failed to process " + directive + + ": expected 4 arguments to a vec4 constructor, got " + parts.length); + } + + try { + consumer.accept(new Vector4f( + Float.parseFloat(parts[0]), + Float.parseFloat(parts[1]), + Float.parseFloat(parts[2]), + Float.parseFloat(parts[3]) + )); + } catch (NumberFormatException e) { + Iris.logger.error("Failed to process " + directive, e); + } + + return; + } + + typeCheckHelper("bool", booleanConstVariables, directive); + typeCheckHelper("int", intConstVariables, directive); + typeCheckHelper("int", stringConstVariables, directive); + typeCheckHelper("float", floatConstVariables, directive); + } + } + + private void typeCheckHelper(String expected, Map candidates, ConstDirectiveParser.ConstDirective directive) { + if (candidates.containsKey(directive.getKey())) { + Iris.logger.warn("Ignoring " + directive + " because it is of the wrong type, a type of " + expected + " is expected."); + } + } + + @Override + public void acceptUniformDirective(String name, Runnable onDetected) { + // TODO + if (IrisLogging.ENABLE_SPAM) { + Iris.logger.warn("Not looking for a uniform directive with the name " + name + " since this type of directive is not currently supported."); + } + } + + @Override + public void acceptCommentStringDirective(String name, Consumer consumer) { + // TODO + if (IrisLogging.ENABLE_SPAM) { + Iris.logger.warn("Not looking for a comment string directive with the name " + name + " since this type of directive is not currently supported."); + } + } + + @Override + public void acceptCommentIntDirective(String name, IntConsumer consumer) { + // TODO + if (IrisLogging.ENABLE_SPAM) { + Iris.logger.warn("Not looking for a comment int directive with the name " + name + " since this type of directive is not currently supported."); + } + } + + @Override + public void acceptCommentFloatDirective(String name, FloatConsumer consumer) { + // TODO + if (IrisLogging.ENABLE_SPAM) { + Iris.logger.warn("Not looking for a comment float directive with the name " + name + " since this type of directive is not currently supported."); + } + } + + @Override + public void acceptConstBooleanDirective(String name, BooleanConsumer consumer) { + booleanConstVariables.put(name, consumer); + } + + @Override + public void acceptConstStringDirective(String name, Consumer consumer) { + stringConstVariables.put(name, consumer); + } + + @Override + public void acceptConstIntDirective(String name, IntConsumer consumer) { + intConstVariables.put(name, consumer); + } + + @Override + public void acceptConstFloatDirective(String name, FloatConsumer consumer) { + floatConstVariables.put(name, consumer); + } + + @Override + public void acceptConstVec2Directive(String name, Consumer consumer) { + vec2ConstVariables.put(name, consumer); + } + + @Override + public void acceptConstIVec3Directive(String name, Consumer consumer) { + ivec3ConstVariables.put(name, consumer); + } + + @Override + public void acceptConstVec4Directive(String name, Consumer consumer) { + vec4ConstVariables.put(name, consumer); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/IdMap.java b/src/main/java/net/coderbot/iris/shaderpack/IdMap.java new file mode 100644 index 000000000..b4dbd6579 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/IdMap.java @@ -0,0 +1,292 @@ +package net.coderbot.iris.shaderpack; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntFunction; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.materialmap.BlockEntry; +import net.coderbot.iris.shaderpack.materialmap.BlockRenderType; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; +import net.coderbot.iris.shaderpack.preprocessor.PropertiesPreprocessor; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +/** + * A utility class for parsing entries in item.properties, block.properties, and entities.properties files in shaderpacks + */ +public class IdMap { + /** + * Maps a given item ID to an integer ID + */ + private final Object2IntMap itemIdMap; + + /** + * Maps a given entity ID to an integer ID + */ + private final Object2IntMap entityIdMap; + + /** + * Maps block states to block ids defined in block.properties + */ + private Int2ObjectMap> blockPropertiesMap; + + /** + * A set of render type overrides for specific blocks. Allows shader packs to move blocks to different render types. + */ + private Map blockRenderTypeMap; + + IdMap(Path shaderPath, ShaderPackOptions shaderPackOptions, Iterable environmentDefines) { + itemIdMap = loadProperties(shaderPath, "item.properties", shaderPackOptions, environmentDefines).map(IdMap::parseItemIdMap).orElse(Object2IntMaps.emptyMap()); + + entityIdMap = loadProperties(shaderPath, "entity.properties", shaderPackOptions, environmentDefines).map(IdMap::parseEntityIdMap).orElse(Object2IntMaps.emptyMap()); + + loadProperties(shaderPath, "block.properties", shaderPackOptions, environmentDefines).ifPresent(blockProperties -> { + blockPropertiesMap = parseBlockMap(blockProperties, "block.", "block.properties"); + blockRenderTypeMap = parseRenderTypeMap(blockProperties, "layer.", "block.properties"); + }); + + // TODO: Properly override block render layers + + if (blockPropertiesMap == null) { + // Fill in with default values... + blockPropertiesMap = new Int2ObjectOpenHashMap<>(); + LegacyIdMap.addLegacyValues(blockPropertiesMap); + } + + if (blockRenderTypeMap == null) { + blockRenderTypeMap = Collections.emptyMap(); + } + } + + /** + * Loads properties from a properties file in a shaderpack path + */ + private static Optional loadProperties(Path shaderPath, String name, ShaderPackOptions shaderPackOptions, + Iterable environmentDefines) { + String fileContents = readProperties(shaderPath, name); + if (fileContents == null) { + return Optional.empty(); + } + + String processed = PropertiesPreprocessor.preprocessSource(fileContents, shaderPackOptions, environmentDefines); + + StringReader propertiesReader = new StringReader(processed); + + // Note: ordering of properties is significant + // See https://github.com/IrisShaders/Iris/issues/1327 and the relevant putIfAbsent calls in + // BlockMaterialMapping + Properties properties = new OrderBackedProperties(); + try { + properties.load(propertiesReader); + } catch (IOException e) { + Iris.logger.error("Error loading " + name + " at " + shaderPath, e); + + return Optional.empty(); + } + + return Optional.of(properties); + } + + private static String readProperties(Path shaderPath, String name) { + try { + // ID maps should be encoded in ISO_8859_1. + return new String(Files.readAllBytes(shaderPath.resolve(name)), StandardCharsets.ISO_8859_1); + } catch (NoSuchFileException e) { + Iris.logger.debug("An " + name + " file was not found in the current shaderpack"); + + return null; + } catch (IOException e) { + Iris.logger.error("An IOException occurred reading " + name + " from the current shaderpack", e); + + return null; + } + } + + private static Object2IntMap parseItemIdMap(Properties properties) { + return parseIdMap(properties, "item.", "item.properties"); + } + + private static Object2IntMap parseEntityIdMap(Properties properties) { + return parseIdMap(properties, "entity.", "entity.properties"); + } + + /** + * Parses a NamespacedId map in OptiFine format + */ + private static Object2IntMap parseIdMap(Properties properties, String keyPrefix, String fileName) { + Object2IntMap idMap = new Object2IntOpenHashMap<>(); + idMap.defaultReturnValue(-1); + + properties.forEach((keyObject, valueObject) -> { + String key = (String) keyObject; + String value = (String) valueObject; + + if (!key.startsWith(keyPrefix)) { + // Not a valid line, ignore it + return; + } + + int intId; + + try { + intId = Integer.parseInt(key.substring(keyPrefix.length())); + } catch (NumberFormatException e) { + // Not a valid property line + Iris.logger.warn("Failed to parse line in " + fileName + ": invalid key " + key); + return; + } + + // Split on any whitespace + for (String part : value.split("\\s+")) { + if (part.contains("=")) { + // Avoid tons of logspam for now + Iris.logger.warn("Failed to parse an ResourceLocation in " + fileName + " for the key " + key + ": state properties are currently not supported: " + part); + continue; + } + + // Note: NamespacedId performs no validation on the content. That will need to be done by whatever is + // converting these things to ResourceLocations. + idMap.put(new NamespacedId(part), intId); + } + }); + + return Object2IntMaps.unmodifiable(idMap); + } + + private static Int2ObjectMap> parseBlockMap(Properties properties, String keyPrefix, String fileName) { + Int2ObjectMap> entriesById = new Int2ObjectOpenHashMap<>(); + + properties.forEach((keyObject, valueObject) -> { + final String key = (String) keyObject; + final String value = (String) valueObject; + + if (!key.startsWith(keyPrefix)) { + // Not a valid line, ignore it + return; + } + + final int intId; + + try { + intId = Integer.parseInt(key.substring(keyPrefix.length())); + } catch (NumberFormatException e) { + // Not a valid property line + Iris.logger.warn("Failed to parse line in " + fileName + ": invalid key " + key); + return; + } + + final List entries = new ArrayList<>(); + + // Split on whitespace groups, not just single spaces + for (String part : value.split("\\s+")) { + if (part.isEmpty()) { + continue; + } + + try { + entries.add(BlockEntry.parse(part)); + } catch (Exception e) { + Iris.logger.warn("Unexpected error while parsing an entry from " + fileName + " for the key " + key + ":", e); + } + } + + entriesById.put(intId, Collections.unmodifiableList(entries)); + }); + + return Int2ObjectMaps.unmodifiable(entriesById); + } + + /** + * Parses a render layer map. + * + * This feature is used by Chocapic v9 and Wisdom Shaders. Otherwise, it is a rarely-used feature. + */ + private static Map parseRenderTypeMap(Properties properties, String keyPrefix, String fileName) { + Map overrides = new HashMap<>(); + + properties.forEach((keyObject, valueObject) -> { + String key = (String) keyObject; + String value = (String) valueObject; + + if (!key.startsWith(keyPrefix)) { + // Not a valid line, ignore it + return; + } + + // Note: We have to remove the prefix "layer." because fromString expects "cutout", not "layer.cutout". + String keyWithoutPrefix = key.substring(keyPrefix.length()); + + BlockRenderType renderType = BlockRenderType.fromString(keyWithoutPrefix).orElse(null); + + if (renderType == null) { + Iris.logger.warn("Failed to parse line in " + fileName + ": invalid block render type: " + key); + return; + } + + for (String part : value.split("\\s+")) { + // Note: NamespacedId performs no validation on the content. That will need to be done by whatever is + // converting these things to ResourceLocations. + overrides.put(new NamespacedId(part), renderType); + } + }); + + return overrides; + } + + public Int2ObjectMap> getBlockProperties() { + return blockPropertiesMap; + } + + public Object2IntFunction getItemIdMap() { + return itemIdMap; + } + + public Object2IntFunction getEntityIdMap() { + return entityIdMap; + } + + public Map getBlockRenderTypeMap() { + return blockRenderTypeMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + IdMap idMap = (IdMap) o; + + return Objects.equals(itemIdMap, idMap.itemIdMap) + && Objects.equals(entityIdMap, idMap.entityIdMap) + && Objects.equals(blockPropertiesMap, idMap.blockPropertiesMap) + && Objects.equals(blockRenderTypeMap, idMap.blockRenderTypeMap); + } + + @Override + public int hashCode() { + return Objects.hash(itemIdMap, entityIdMap, blockPropertiesMap, blockRenderTypeMap); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/IrisLimits.java b/src/main/java/net/coderbot/iris/shaderpack/IrisLimits.java new file mode 100644 index 000000000..b97143a94 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/IrisLimits.java @@ -0,0 +1,12 @@ +package net.coderbot.iris.shaderpack; + +public class IrisLimits { + /** + * The maximum number of color textures that a shader pack can write to and read from in gbuffer and composite + * programs. + * + * It's not recommended to raise this higher than 16 until code for avoiding allocation of unused color textures + * is implemented. + */ + public static final int MAX_COLOR_BUFFERS = 16; +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/LanguageMap.java b/src/main/java/net/coderbot/iris/shaderpack/LanguageMap.java new file mode 100644 index 000000000..c1468acfc --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/LanguageMap.java @@ -0,0 +1,73 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.Iris; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Stream; + +public class LanguageMap { + private final Map> translationMaps; + + public LanguageMap(Path root) throws IOException { + this.translationMaps = new HashMap<>(); + + if (!Files.exists(root)) { + return; + } + + // We are using a max depth of one to ensure we only get the surface level *files* without going deeper + // we also want to avoid any directories while filtering + // Basically, we want the immediate files nested in the path for the langFolder + // There is also Files.list which can be used for similar behavior + try (Stream stream = Files.list(root)) { + stream.filter(path -> !Files.isDirectory(path)).forEach(path -> { + // Also note that OptiFine uses a property scheme for loading language entries to keep parity with other + // OptiFine features + final String currentFileName = path.getFileName().toString(); + + if (!currentFileName.endsWith(".lang")) { + // This file lacks a .lang file extension and should be ignored. + return; + } + + final String currentLangCode = currentFileName.substring(0, currentFileName.lastIndexOf(".")); + final Properties properties = new Properties(); + + // Use InputStreamReader to avoid the default charset of ISO-8859-1. + // This is needed since shader language files are specified to be in UTF-8. + try (InputStreamReader isr = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) { + properties.load(isr); + } catch (IOException e) { + Iris.logger.error("Failed to parse shader pack language file " + path, e); + } + + final ImmutableMap.Builder builder = ImmutableMap.builder(); + + properties.forEach((key, value) -> builder.put(key.toString(), value.toString())); + + translationMaps.put(currentLangCode, builder.build()); + }); + } + } + + public Set getLanguages() { + // Ensure that the caller can't mess with the language map. + return Collections.unmodifiableSet(translationMaps.keySet()); + } + + public Map getTranslations(String language) { + // We're returning an immutable map, so the caller can't modify it. + return translationMaps.get(language); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/LegacyIdMap.java b/src/main/java/net/coderbot/iris/shaderpack/LegacyIdMap.java new file mode 100644 index 000000000..615091caa --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/LegacyIdMap.java @@ -0,0 +1,98 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.Function; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.coderbot.iris.shaderpack.materialmap.BlockEntry; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class LegacyIdMap { + private static final ImmutableList COLORS = + ImmutableList.of("white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", + "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black"); + + private static final ImmutableList WOOD_TYPES = + ImmutableList.of("oak", "birch", "jungle", "spruce", "acacia", "dark_oak"); + + public static void addLegacyValues(Int2ObjectMap> blockIdMap) { + add(blockIdMap, 1, block("stone"), block("granite"), block("diorite"), block("andesite")); + add(blockIdMap, 2, block("grass_block")); + add(blockIdMap, 4, block("cobblestone")); + + add(blockIdMap, 50, block("torch")); + add(blockIdMap, 89, block("glowstone")); + + // TODO: what about inactive redstone lamps? + add(blockIdMap, 124, block("redstone_lamp")); + + add(blockIdMap, 12, block("sand")); + add(blockIdMap, 24, block("sandstone")); + + add(blockIdMap, 41, block("gold_block")); + add(blockIdMap, 42, block("iron_block")); + add(blockIdMap, 57, block("diamond_block")); + // Apparently this is what SEUS v11 expects? Maybe old shadersmod was buggy. + add(blockIdMap, -123, block("emerald_block")); + + addMany(blockIdMap, 35, COLORS, color -> block(color + "_wool")); + + // NB: Use the "still" IDs for water and lava, since some shader packs don't properly support the "flowing" + // versions: https://github.com/IrisShaders/Iris/issues/1462 + add(blockIdMap, 9, block("water")); + add(blockIdMap, 11, block("lava")); + add(blockIdMap, 79, block("ice")); + + addMany(blockIdMap, 18, WOOD_TYPES, woodType -> block(woodType + "_leaves")); + + addMany(blockIdMap, 95, COLORS, color -> block(color + "_stained_glass")); + addMany(blockIdMap, 160, COLORS, color -> block(color + "_stained_glass_pane")); + + // Short grass / bush + add(blockIdMap, 31, block("grass"), block("seagrass"), block("sweet_berry_bush")); + + // Crops (59 = wheat), but we include carrots and potatoes too. + add(blockIdMap, 59, block("wheat"), block("carrots"), block("potatoes")); + + // Small flowers + add(blockIdMap, 37, block("dandelion"), block("poppy"), block("blue_orchid"), + block("allium"), block("azure_bluet"), block("red_tulip"), block("pink_tulip"), + block("white_tulip"), block("orange_tulip"), block("oxeye_daisy"), + block("cornflower"), block("lily_of_the_valley"), block("wither_rose")); + + // Big tall grass / flowers + // Also include seagrass here + add(blockIdMap, 175, block("sunflower"), block("lilac"), block("tall_grass"), + block("large_fern"), block("rose_bush"), block("peony"), block("tall_seagrass")); + + // Fire + add(blockIdMap, 51, block("fire")); + + // Lily pad + add(blockIdMap, 111, block("lily_pad")); + + // TODO: 76 -> redstone_torch (on) + } + + private static BlockEntry block(String name) { + return new BlockEntry(new NamespacedId("minecraft", name), Collections.emptySet()); + } + + private static void addMany(Int2ObjectMap> blockIdMap, int id, List prefixes, Function toId) { + List entries = new ArrayList<>(); + + for (String prefix : prefixes) { + entries.add(toId.apply(prefix)); + } + + blockIdMap.put(id, entries); + } + + private static void add(Int2ObjectMap> blockIdMap, int id, BlockEntry... entries) { + blockIdMap.put(id, Arrays.asList(entries)); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/OptionalBoolean.java b/src/main/java/net/coderbot/iris/shaderpack/OptionalBoolean.java new file mode 100644 index 000000000..390eac4ec --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/OptionalBoolean.java @@ -0,0 +1,25 @@ +package net.coderbot.iris.shaderpack; + +import java.util.function.BooleanSupplier; + +public enum OptionalBoolean { + DEFAULT, + FALSE, + TRUE; + + public boolean orElse(boolean defaultValue) { + if (this == DEFAULT) { + return defaultValue; + } + + return this == TRUE; + } + + public boolean orElseGet(BooleanSupplier defaultValue) { + if (this == DEFAULT) { + return defaultValue.getAsBoolean(); + } + + return this == TRUE; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/OrderBackedProperties.java b/src/main/java/net/coderbot/iris/shaderpack/OrderBackedProperties.java new file mode 100644 index 000000000..5c391db42 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/OrderBackedProperties.java @@ -0,0 +1,27 @@ +package net.coderbot.iris.shaderpack; + +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; + +import java.util.Map; +import java.util.Properties; +import java.util.function.BiConsumer; + +/** + * Properties backed by a {@link java.util.LinkedHashMap}, in order to preserve iteration order + */ +public class OrderBackedProperties extends Properties { + private transient final Map backing = Object2ObjectMaps.synchronize(new Object2ObjectLinkedOpenHashMap<>()); + + @Override + public synchronized Object put(Object key, Object value) { + backing.put(key, value); + + return super.put(key, value); + } + + @Override + public synchronized void forEach(BiConsumer action) { + this.backing.forEach(action); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java b/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java new file mode 100644 index 000000000..18a5e40b2 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java @@ -0,0 +1,216 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.texture.TextureScaleOverride; +import org.joml.Vector2i; + +import java.util.Set; + +public class PackDirectives { + @Getter + private int noiseTextureResolution; + @Getter + private float sunPathRotation; + @Getter + private float ambientOcclusionLevel; + @Getter + private float wetnessHalfLife; + @Getter + private float drynessHalfLife; + @Getter + private float eyeBrightnessHalfLife; + @Getter + private float centerDepthHalfLife; + @Getter + private CloudSetting cloudSetting; + private boolean underwaterOverlay; + private boolean vignette; + private boolean sun; + private boolean moon; + private boolean rainDepth; + private boolean separateAo; + @Getter + private boolean oldLighting; + private boolean concurrentCompute; + @Getter + private boolean oldHandLight; + private boolean particlesBeforeDeferred; + @Getter + private boolean prepareBeforeShadow; + private Object2ObjectMap> explicitFlips = new Object2ObjectOpenHashMap<>(); + private Object2ObjectMap scaleOverrides = new Object2ObjectOpenHashMap<>(); + + @Getter + private final PackRenderTargetDirectives renderTargetDirectives; + @Getter + private final PackShadowDirectives shadowDirectives; + + private PackDirectives(Set supportedRenderTargets, PackShadowDirectives packShadowDirectives) { + noiseTextureResolution = 256; + sunPathRotation = 0.0F; + ambientOcclusionLevel = 1.0F; + wetnessHalfLife = 600.0f; + drynessHalfLife = 200.0f; + eyeBrightnessHalfLife = 10.0f; + centerDepthHalfLife = 1.0F; + renderTargetDirectives = new PackRenderTargetDirectives(supportedRenderTargets); + shadowDirectives = packShadowDirectives; + } + + PackDirectives(Set supportedRenderTargets, ShaderProperties properties) { + this(supportedRenderTargets, new PackShadowDirectives(properties)); + cloudSetting = properties.getCloudSetting(); + underwaterOverlay = properties.getUnderwaterOverlay().orElse(false); + vignette = properties.getVignette().orElse(false); + sun = properties.getSun().orElse(true); + moon = properties.getMoon().orElse(true); + rainDepth = properties.getRainDepth().orElse(false); + separateAo = properties.getSeparateAo().orElse(false); + oldLighting = properties.getOldLighting().orElse(false); + concurrentCompute = properties.getConcurrentCompute().orElse(false); + oldHandLight = properties.getOldHandLight().orElse(true); + explicitFlips = properties.getExplicitFlips(); + scaleOverrides = properties.getTextureScaleOverrides(); + particlesBeforeDeferred = properties.getParticlesBeforeDeferred().orElse(false); + prepareBeforeShadow = properties.getPrepareBeforeShadow().orElse(false); + } + + PackDirectives(Set supportedRenderTargets, PackDirectives directives) { + this(supportedRenderTargets, new PackShadowDirectives(directives.getShadowDirectives())); + cloudSetting = directives.cloudSetting; + separateAo = directives.separateAo; + oldLighting = directives.oldLighting; + concurrentCompute = directives.concurrentCompute; + explicitFlips = directives.explicitFlips; + scaleOverrides = directives.scaleOverrides; + particlesBeforeDeferred = directives.particlesBeforeDeferred; + prepareBeforeShadow = directives.prepareBeforeShadow; + } + + public boolean underwaterOverlay() { + return underwaterOverlay; + } + + public boolean vignette() { + return vignette; + } + + public boolean shouldRenderSun() { + return sun; + } + + public boolean shouldRenderMoon() { + return moon; + } + + public boolean rainDepth() { + return rainDepth; + } + + public boolean shouldUseSeparateAo() { + return separateAo; + } + + public boolean areParticlesBeforeDeferred() { + return particlesBeforeDeferred; + } + + public boolean getConcurrentCompute() { + return concurrentCompute; + } + + private static float clamp(float val, float lo, float hi) { + return Math.max(lo, Math.min(hi, val)); + } + + public void acceptDirectivesFrom(DirectiveHolder directives) { + renderTargetDirectives.acceptDirectives(directives); + shadowDirectives.acceptDirectives(directives); + + directives.acceptConstIntDirective("noiseTextureResolution", + noiseTextureResolution -> this.noiseTextureResolution = noiseTextureResolution); + + directives.acceptConstFloatDirective("sunPathRotation", + sunPathRotation -> this.sunPathRotation = sunPathRotation); + + directives.acceptConstFloatDirective("ambientOcclusionLevel", + ambientOcclusionLevel -> this.ambientOcclusionLevel = clamp(ambientOcclusionLevel, 0.0f, 1.0f)); + + directives.acceptConstFloatDirective("wetnessHalflife", + wetnessHalfLife -> this.wetnessHalfLife = wetnessHalfLife); + + directives.acceptConstFloatDirective("drynessHalflife", + wetnessHalfLife -> this.wetnessHalfLife = wetnessHalfLife); + + directives.acceptConstFloatDirective("eyeBrightnessHalflife", + eyeBrightnessHalfLife -> this.eyeBrightnessHalfLife = eyeBrightnessHalfLife); + + directives.acceptConstFloatDirective("centerDepthHalflife", + centerDepthHalfLife -> this.centerDepthHalfLife = centerDepthHalfLife); + } + + public ImmutableMap getExplicitFlips(String pass) { + ImmutableMap.Builder explicitFlips = ImmutableMap.builder(); + + Object2BooleanMap explicitFlipsStr = this.explicitFlips.get(pass); + + if (explicitFlipsStr == null) { + explicitFlipsStr = Object2BooleanMaps.emptyMap(); + } + + explicitFlipsStr.forEach((buffer, shouldFlip) -> { + int index = PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.indexOf(buffer); + + if (index == -1 && buffer.startsWith("colortex")) { + final String id = buffer.substring("colortex".length()); + + try { + index = Integer.parseInt(id); + } catch (NumberFormatException e) { + // fall through to index == null check for unknown buffer. + } + } + + if (index != -1) { + explicitFlips.put(index, shouldFlip); + } else { + Iris.logger.warn("Unknown buffer with ID " + buffer + " specified in flip directive for pass " + + pass); + } + }); + + return explicitFlips.build(); + } + + public Vector2i getTextureScaleOverride(int index, int dimensionX, int dimensionY) { + final String name = "colortex" + index; + + // TODO: How do custom textures interact with aliases? + + final Vector2i scale = new Vector2i(); + + if (index < PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.size()) { + final String legacyName = PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.get(index); + + if (scaleOverrides.containsKey(legacyName)) { + scale.set(scaleOverrides.get(legacyName).getX(dimensionX), scaleOverrides.get(legacyName).getY(dimensionY)); + } else if (scaleOverrides.containsKey(name)) { + scale.set(scaleOverrides.get(name).getX(dimensionX), scaleOverrides.get(name).getY(dimensionY)); + } else { + scale.set(dimensionX, dimensionY); + } + } else if (scaleOverrides.containsKey(name)) { + scale.set(scaleOverrides.get(name).getX(dimensionX), scaleOverrides.get(name).getY(dimensionY)); + } else { + scale.set(dimensionX, dimensionY); + } + + return scale; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/PackRenderTargetDirectives.java b/src/main/java/net/coderbot/iris/shaderpack/PackRenderTargetDirectives.java new file mode 100644 index 000000000..3276e564e --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/PackRenderTargetDirectives.java @@ -0,0 +1,161 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import org.joml.Vector4f; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class PackRenderTargetDirectives { + public static final ImmutableList LEGACY_RENDER_TARGETS = ImmutableList.of( + "gcolor", + "gdepth", + "gnormal", + "composite", + "gaux1", + "gaux2", + "gaux3", + "gaux4" + ); + + // TODO: Instead of just passing this, the shader pack loader should try to figure out what color buffers are in + // use. + public static final Set BASELINE_SUPPORTED_RENDER_TARGETS; + + static { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (int i = 0; i < IrisLimits.MAX_COLOR_BUFFERS; i++) { + builder.add(i); + } + + BASELINE_SUPPORTED_RENDER_TARGETS = builder.build(); + } + + private final Int2ObjectMap renderTargetSettings; + + PackRenderTargetDirectives(Set supportedRenderTargets) { + this.renderTargetSettings = new Int2ObjectOpenHashMap<>(); + + supportedRenderTargets.forEach( + (index) -> renderTargetSettings.put(index.intValue(), new RenderTargetSettings())); + } + + public IntList getBuffersToBeCleared() { + IntList buffersToBeCleared = new IntArrayList(); + + renderTargetSettings.forEach((index, settings) -> { + if (settings.shouldClear()) { + buffersToBeCleared.add(index.intValue()); + } + }); + + return buffersToBeCleared; + } + + public Map getRenderTargetSettings() { + return Collections.unmodifiableMap(renderTargetSettings); + } + + public void acceptDirectives(DirectiveHolder directives) { + Optional.ofNullable(renderTargetSettings.get(7)).ifPresent(colortex7 -> { + // Handle legacy GAUX4FORMAT directives + + directives.acceptCommentStringDirective("GAUX4FORMAT", format -> { + if ("RGBA32F".equals(format)) { + colortex7.requestedFormat = InternalTextureFormat.RGBA32F; + } else if ("RGB32F".equals(format)) { + colortex7.requestedFormat = InternalTextureFormat.RGB32F; + } else if ("RGB16".equals(format)) { + colortex7.requestedFormat = InternalTextureFormat.RGB16; + } else { + Iris.logger.warn("Ignoring GAUX4FORMAT directive /* GAUX4FORMAT:" + format + "*/ because " + format + + " must be RGBA32F, RGB32F, or RGB16. Use `const int colortex7Format = " + format + ";` + instead."); + } + }); + }); + + // If a shaderpack declares a gdepth uniform (even if it is not actually sampled or even of the correct type), + // we upgrade the format of gdepth / colortex1 to RGBA32F if it is currently RGBA. + Optional.ofNullable(renderTargetSettings.get(1)).ifPresent(gdepth -> { + directives.acceptUniformDirective("gdepth", () -> { + if (gdepth.requestedFormat == InternalTextureFormat.RGBA) { + gdepth.requestedFormat = InternalTextureFormat.RGBA32F; + } + }); + }); + + renderTargetSettings.forEach((index, settings) -> { + acceptBufferDirectives(directives, settings, "colortex" + index); + + if (index < LEGACY_RENDER_TARGETS.size()) { + acceptBufferDirectives(directives, settings, LEGACY_RENDER_TARGETS.get(index)); + } + }); + } + + private void acceptBufferDirectives(DirectiveHolder directives, RenderTargetSettings settings, String bufferName) { + directives.acceptConstStringDirective(bufferName + "Format", format -> { + Optional internalFormat = InternalTextureFormat.fromString(format); + + if (internalFormat.isPresent()) { + settings.requestedFormat = internalFormat.get(); + } else { + Iris.logger.warn("Unrecognized internal texture format " + format + " specified for " + bufferName + "Format, ignoring."); + } + }); + + // TODO: Only for composite and deferred + directives.acceptConstBooleanDirective(bufferName + "Clear", + shouldClear -> settings.clear = shouldClear); + + // TODO: Only for composite, deferred, and final + + // Note: This is still relevant even if shouldClear is false, + // since this will be the initial color of the buffer. + directives.acceptConstVec4Directive(bufferName + "ClearColor", + clearColor -> settings.clearColor = clearColor); + } + + public static final class RenderTargetSettings { + private InternalTextureFormat requestedFormat; + private boolean clear; + private Vector4f clearColor; + + public RenderTargetSettings() { + this.requestedFormat = InternalTextureFormat.RGBA; + this.clear = true; + this.clearColor = null; + } + + public InternalTextureFormat getInternalFormat() { + return requestedFormat; + } + + public boolean shouldClear() { + return clear; + } + + public Optional getClearColor() { + return Optional.ofNullable(clearColor); + } + + @Override + public String toString() { + return "RenderTargetSettings{" + + "requestedFormat=" + requestedFormat + + ", clear=" + clear + + ", clearColor=" + clearColor + + '}'; + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/PackShadowDirectives.java b/src/main/java/net/coderbot/iris/shaderpack/PackShadowDirectives.java new file mode 100644 index 000000000..e216c1639 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/PackShadowDirectives.java @@ -0,0 +1,450 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableList; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import org.joml.Vector4f; + +import java.util.Optional; + +public class PackShadowDirectives { + // Bump this up if you want more shadow color buffers! + // This is currently set at 2 for ShadersMod / OptiFine parity but can theoretically be bumped up to 8. + // TODO: Make this configurable? + public static final int MAX_SHADOW_COLOR_BUFFERS = 2; + + private final OptionalBoolean shadowEnabled; + + private int resolution; + // Use a boxed form so we can use null to indicate that there is not an FOV specified. + private Float fov; + private float distance; + private float distanceRenderMul; + private float entityShadowDistanceMul; + private boolean explicitRenderDistance; + private float intervalSize; + + private final boolean shouldRenderTerrain; + private final boolean shouldRenderTranslucent; + private final boolean shouldRenderEntities; + private final boolean shouldRenderPlayer; + private final boolean shouldRenderBlockEntities; + private final OptionalBoolean cullingState; + + private final ImmutableList depthSamplingSettings; + private final ImmutableList colorSamplingSettings; + + public PackShadowDirectives(ShaderProperties properties) { + // By default, the shadow map has a resolution of 1024x1024. It's recommended to increase this for better + // quality. + this.resolution = 1024; + + // By default, shadows do not use FOV, and instead use an orthographic projection controlled by shadowDistance + // + // If FOV is defined, shadows will use a perspective projection controlled by the FOV, and shadowDistance will + // be disregarded for the purposes of creating the projection matrix. However, it will still be used to figure + // out the render distance for shadows if shadowRenderDistanceMul is greater than zero. + this.fov = null; + + // By default, an orthographic projection with a half plane of 160 meters is used, corresponding to a render + // distance of 10 chunks. + // + // It's recommended for shader pack authors to lower this setting to meet their needs in addition to setting + // shadowRenderDistanceMul to a nonzero value, since having a high shadow render distance will impact + // performance quite heavily on most systems. + this.distance = 160.0f; + + // By default, shadows are not culled based on distance from the player. However, pack authors may + // enable this by setting shadowRenderDistanceMul to a nonzero value. + // + // Culling shadows based on the shadow matrices is often infeasible because shader packs frequently + // employ non-linear transformations that end up fitting more far away chunks into the shadow map, + // as well as giving higher detail to close up chunks. + // + // However, Iris does still does cull shadows whenever it can - but, it does so by analyzing + // whether or not shadows can possibly be cast into the player's view, instead of just checking + // the shadow matrices. + this.distanceRenderMul = -1.0f; + this.entityShadowDistanceMul = 1.0f; + this.explicitRenderDistance = false; + + // By default, a shadow interval size of 2 meters is used. This means that the shadow camera will be snapped to + // a grid where each grid cell is 2 meters by 2 meters by 2 meters, and it will only move either when the sun / + // moon move, or when the player camera moves into a different grid cell. + this.intervalSize = 2.0f; + + this.shouldRenderTerrain = properties.getShadowTerrain().orElse(true); + this.shouldRenderTranslucent = properties.getShadowTranslucent().orElse(true); + this.shouldRenderEntities = properties.getShadowEntities().orElse(true); + this.shouldRenderPlayer = properties.getShadowPlayer().orElse(false); + this.shouldRenderBlockEntities = properties.getShadowBlockEntities().orElse(true); + this.cullingState = properties.getShadowCulling(); + this.shadowEnabled = properties.getShadowEnabled(); + + this.depthSamplingSettings = ImmutableList.of(new DepthSamplingSettings(), new DepthSamplingSettings()); + + ImmutableList.Builder colorSamplingSettings = ImmutableList.builder(); + + for (int i = 0; i < MAX_SHADOW_COLOR_BUFFERS; i++) { + colorSamplingSettings.add(new SamplingSettings()); + } + + this.colorSamplingSettings = colorSamplingSettings.build(); + } + + public PackShadowDirectives(PackShadowDirectives shadowDirectives) { + this.resolution = shadowDirectives.resolution; + this.fov = shadowDirectives.fov; + this.distance = shadowDirectives.distance; + this.distanceRenderMul = shadowDirectives.distanceRenderMul; + this.entityShadowDistanceMul = shadowDirectives.entityShadowDistanceMul; + this.explicitRenderDistance = shadowDirectives.explicitRenderDistance; + this.intervalSize = shadowDirectives.intervalSize; + this.shouldRenderTerrain = shadowDirectives.shouldRenderTerrain; + this.shouldRenderTranslucent = shadowDirectives.shouldRenderTranslucent; + this.shouldRenderEntities = shadowDirectives.shouldRenderEntities; + this.shouldRenderPlayer = shadowDirectives.shouldRenderPlayer; + this.shouldRenderBlockEntities = shadowDirectives.shouldRenderBlockEntities; + this.cullingState = shadowDirectives.cullingState; + this.depthSamplingSettings = shadowDirectives.depthSamplingSettings; + this.colorSamplingSettings = shadowDirectives.colorSamplingSettings; + this.shadowEnabled = shadowDirectives.shadowEnabled; + } + + public int getResolution() { + return resolution; + } + + public Float getFov() { + return fov; + } + + public float getDistance() { + return distance; + } + + public float getDistanceRenderMul() { + return distanceRenderMul; + } + + public float getEntityShadowDistanceMul() { + return entityShadowDistanceMul; + } + + public boolean isDistanceRenderMulExplicit() { + return explicitRenderDistance; + } + + public float getIntervalSize() { + return intervalSize; + } + + public boolean shouldRenderTerrain() { + return shouldRenderTerrain; + } + + public boolean shouldRenderTranslucent() { + return shouldRenderTranslucent; + } + + public boolean shouldRenderEntities() { + return shouldRenderEntities; + } + + public boolean shouldRenderPlayer() { + return shouldRenderPlayer; + } + + public boolean shouldRenderBlockEntities() { + return shouldRenderBlockEntities; + } + + public OptionalBoolean getCullingState() { + return cullingState; + } + + public OptionalBoolean isShadowEnabled() { + return shadowEnabled; + } + + public ImmutableList getDepthSamplingSettings() { + return depthSamplingSettings; + } + + public ImmutableList getColorSamplingSettings() { + return colorSamplingSettings; + } + + public void acceptDirectives(DirectiveHolder directives) { + directives.acceptCommentIntDirective("SHADOWRES", resolution -> this.resolution = resolution); + directives.acceptConstIntDirective("shadowMapResolution", resolution -> this.resolution = resolution); + + directives.acceptCommentFloatDirective("SHADOWFOV", fov -> this.fov = fov); + directives.acceptConstFloatDirective("shadowMapFov", fov -> this.fov = fov); + + directives.acceptCommentFloatDirective("SHADOWHPL", distance -> this.distance = distance); + directives.acceptConstFloatDirective("shadowDistance", distance -> this.distance = distance); + + directives.acceptConstFloatDirective("entityShadowDistanceMul", distance -> this.entityShadowDistanceMul = distance); + + directives.acceptConstFloatDirective("shadowDistanceRenderMul", distanceRenderMul -> { + this.distanceRenderMul = distanceRenderMul; + this.explicitRenderDistance = true; + }); + + directives.acceptConstFloatDirective("shadowIntervalSize", + intervalSize -> this.intervalSize = intervalSize); + + acceptHardwareFilteringSettings(directives, depthSamplingSettings); + acceptDepthMipmapSettings(directives, depthSamplingSettings); + acceptColorMipmapSettings(directives, colorSamplingSettings); + acceptDepthFilteringSettings(directives, depthSamplingSettings); + acceptColorFilteringSettings(directives, colorSamplingSettings); + acceptBufferDirectives(directives, colorSamplingSettings); + } + + /** + * Handles shadowHardwareFiltering* directives + */ + private static void acceptHardwareFilteringSettings(DirectiveHolder directives, ImmutableList samplers) { + // Get the default base value for the hardware filtering setting + directives.acceptConstBooleanDirective("shadowHardwareFiltering", hardwareFiltering -> { + for (DepthSamplingSettings samplerSettings : samplers) { + samplerSettings.setHardwareFiltering(hardwareFiltering); + } + }); + + // Find any per-sampler overrides for the hardware filtering setting + for (int i = 0; i < samplers.size(); i++) { + String name = "shadowHardwareFiltering" + i; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setHardwareFiltering); + } + } + + private static void acceptDepthMipmapSettings(DirectiveHolder directives, ImmutableList samplers) { + // Get the default base value for the shadow depth mipmap setting + directives.acceptConstBooleanDirective("generateShadowMipmap", mipmap -> { + for (SamplingSettings samplerSettings : samplers) { + samplerSettings.setMipmap(mipmap); + } + }); + + // Find any per-sampler overrides for the shadow depth mipmap setting + + // Legacy override option: shadowtexMipmap, an alias for shadowtex0Mipmap + if (samplers.size() >= 1) { + directives.acceptConstBooleanDirective("shadowtexMipmap", samplers.get(0)::setMipmap); + } + + // Standard override option: shadowtex0Mipmap and shadowtex1Mipmap + for (int i = 0; i < samplers.size(); i++) { + String name = "shadowtex" + i + "Mipmap"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setMipmap); + } + } + + private static void acceptColorMipmapSettings(DirectiveHolder directives, ImmutableList samplers) { + // Get the default base value for the shadow depth mipmap setting + directives.acceptConstBooleanDirective("generateShadowColorMipmap", mipmap -> { + for (SamplingSettings samplerSettings : samplers) { + samplerSettings.setMipmap(mipmap); + } + }); + + // Find any per-sampler overrides for the shadow depth mipmap setting + for (int i = 0; i < samplers.size(); i++) { + String name = "shadowcolor" + i + "Mipmap"; + directives.acceptConstBooleanDirective(name, samplers.get(i)::setMipmap); + + name = "shadowColor" + i + "Mipmap"; + directives.acceptConstBooleanDirective(name, samplers.get(i)::setMipmap); + } + } + + private static void acceptDepthFilteringSettings(DirectiveHolder directives, ImmutableList samplers) { + if (samplers.size() >= 1) { + directives.acceptConstBooleanDirective("shadowtexNearest", samplers.get(0)::setNearest); + } + + for (int i = 0; i < samplers.size(); i++) { + String name = "shadowtex" + i + "Nearest"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setNearest); + + name = "shadow" + i + "MinMagNearest"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setNearest); + } + } + + private static void acceptColorFilteringSettings(DirectiveHolder directives, ImmutableList samplers) { + for (int i = 0; i < samplers.size(); i++) { + String name = "shadowcolor" + i + "Nearest"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setNearest); + + name = "shadowColor" + i + "Nearest"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setNearest); + + name = "shadowColor" + i + "MinMagNearest"; + + directives.acceptConstBooleanDirective(name, samplers.get(i)::setNearest); + } + } + + private void acceptBufferDirectives(DirectiveHolder directives, ImmutableList settings) { + for (int i = 0; i < settings.size(); i++) { + String bufferName = "shadowcolor" + i; + int finalI = i; + directives.acceptConstStringDirective(bufferName + "Format", format -> { + Optional internalFormat = InternalTextureFormat.fromString(format); + + if (internalFormat.isPresent()) { + settings.get(finalI).setFormat(internalFormat.get()); + } else { + Iris.logger.warn("Unrecognized internal texture format " + format + " specified for " + bufferName + "Format, ignoring."); + } + }); + + // TODO: Only for composite and deferred + directives.acceptConstBooleanDirective(bufferName + "Clear", + shouldClear -> settings.get(finalI).setClear(shouldClear)); + + // TODO: Only for composite, deferred, and final + + // Note: This is still relevant even if shouldClear is false, + // since this will be the initial color of the buffer. + directives.acceptConstVec4Directive(bufferName + "ClearColor", + clearColor -> settings.get(finalI).setClearColor(clearColor)); + } + } + + @Override + public String toString() { + return "PackShadowDirectives{" + + "resolution=" + resolution + + ", fov=" + fov + + ", distance=" + distance + + ", distanceRenderMul=" + distanceRenderMul + + ", entityDistanceRenderMul=" + entityShadowDistanceMul + + ", intervalSize=" + intervalSize + + ", depthSamplingSettings=" + depthSamplingSettings + + ", colorSamplingSettings=" + colorSamplingSettings + + '}'; + } + + public static class SamplingSettings { + /** + * Whether mipmaps should be generated before sampling. Disabled by default. + */ + private boolean mipmap; + + /** + * Whether nearest texture filtering should be used in place of linear filtering. By default, linear filtering + * is used, which applies some blur, but if this is not desired behavior, nearest filtering can be used. + */ + private boolean nearest; + + /** + * Whether to clear the buffer every frame. + */ + private boolean clear; + + /** + * The color to clear the buffer to. If {@code clear} is false, this has no effect. + */ + private Vector4f clearColor; + + /** + * The internal format to use for the color buffer. + */ + private InternalTextureFormat format; + + public SamplingSettings() { + mipmap = false; + nearest = false; + clear = true; + clearColor = new Vector4f(1.0F); + format = InternalTextureFormat.RGBA; + } + + protected void setMipmap(boolean mipmap) { + this.mipmap = mipmap; + } + + protected void setNearest(boolean nearest) { + this.nearest = nearest; + } + + protected void setClear(boolean clear) { + this.clear = clear; + } + + protected void setClearColor(Vector4f clearColor) { + this.clearColor = clearColor; + } + + protected void setFormat(InternalTextureFormat format) { + this.format = format; + } + + public boolean getMipmap() { + return this.mipmap; + } + + public boolean getNearest() { + return this.nearest; + } + + public boolean getClear() { + return clear; + } + + public Vector4f getClearColor() { + return clearColor; + } + + public InternalTextureFormat getFormat() { + return this.format; + } + + @Override + public String toString() { + return "SamplingSettings{" + + "mipmap=" + mipmap + + ", nearest=" + nearest + + ", clear=" + clear + + ", clearColor=" + clearColor + + ", format=" + format.name() + + '}'; + } + } + + public static class DepthSamplingSettings extends SamplingSettings { + private boolean hardwareFiltering; + + public DepthSamplingSettings() { + hardwareFiltering = false; + } + + private void setHardwareFiltering(boolean hardwareFiltering) { + this.hardwareFiltering = hardwareFiltering; + } + + public boolean getHardwareFiltering() { + return hardwareFiltering; + } + + @Override + public String toString() { + return "DepthSamplingSettings{" + + "mipmap=" + getMipmap() + + ", nearest=" + getNearest() + + ", hardwareFiltering=" + hardwareFiltering + + '}'; + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ProgramDirectives.java b/src/main/java/net/coderbot/iris/shaderpack/ProgramDirectives.java new file mode 100644 index 000000000..6a121cb59 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ProgramDirectives.java @@ -0,0 +1,186 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import net.coderbot.iris.gl.blending.AlphaTestOverride; +import net.coderbot.iris.gl.blending.BlendModeOverride; +import net.coderbot.iris.gl.blending.BufferBlendInformation; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class ProgramDirectives { + private static final ImmutableList LEGACY_RENDER_TARGETS = PackRenderTargetDirectives.LEGACY_RENDER_TARGETS; + + private final int[] drawBuffers; + private final float viewportScale; + @Nullable + private final AlphaTestOverride alphaTestOverride; + + private final Optional blendModeOverride; + private final List bufferBlendInformations; + private final ImmutableSet mipmappedBuffers; + private final ImmutableMap explicitFlips; + + private ProgramDirectives(int[] drawBuffers, float viewportScale, @Nullable AlphaTestOverride alphaTestOverride, + Optional blendModeOverride, List bufferBlendInformations, ImmutableSet mipmappedBuffers, + ImmutableMap explicitFlips) { + this.drawBuffers = drawBuffers; + this.viewportScale = viewportScale; + this.alphaTestOverride = alphaTestOverride; + this.blendModeOverride = blendModeOverride; + this.bufferBlendInformations = bufferBlendInformations; + this.mipmappedBuffers = mipmappedBuffers; + this.explicitFlips = explicitFlips; + } + + ProgramDirectives(ProgramSource source, ShaderProperties properties, Set supportedRenderTargets, + @Nullable BlendModeOverride defaultBlendOverride) { + // DRAWBUFFERS is only detected in the fragment shader source code (.fsh). + // If there's no explicit declaration, then by default /* DRAWBUFFERS:0 */ is inferred. + // For SEUS v08 and SEUS v10 to work, this will need to be set to 01234567. However, doing this causes + // TAA to break on Sildur's Vibrant Shaders, since gbuffers_skybasic lacks a DRAWBUFFERS directive, causing + // undefined data to be written to colortex7. + // + // TODO: Figure out how to infer the DRAWBUFFERS directive when it is missing. + Optional optionalDrawbuffersDirective = findDrawbuffersDirective(source.getFragmentSource()); + Optional optionalRendertargetsDirective = findRendertargetsDirective(source.getFragmentSource()); + + Optional optionalCommentDirective = getAppliedDirective(optionalDrawbuffersDirective, optionalRendertargetsDirective); + drawBuffers = optionalCommentDirective.map(commentDirective -> { + if (commentDirective.getType() == CommentDirective.Type.DRAWBUFFERS) { + return parseDigits(commentDirective.getDirective().toCharArray()); + } else if (commentDirective.getType() == CommentDirective.Type.RENDERTARGETS) { + return parseDigitList(commentDirective.getDirective()); + } else { + throw new IllegalStateException("Unhandled comment directive type!"); + } + }).orElse(new int[] { 0 }); + + if (properties != null) { + viewportScale = properties.getViewportScaleOverrides().getOrDefault(source.getName(), 1.0f); + alphaTestOverride = properties.getAlphaTestOverrides().get(source.getName()); + + BlendModeOverride blendModeOverride = properties.getBlendModeOverrides().get(source.getName()); + List bufferBlendInformations = properties.getBufferBlendOverrides().get(source.getName()); + this.blendModeOverride = Optional.ofNullable(blendModeOverride != null ? blendModeOverride : defaultBlendOverride); + this.bufferBlendInformations = bufferBlendInformations != null ? bufferBlendInformations : Collections.emptyList(); + + explicitFlips = source.getParent().getPackDirectives().getExplicitFlips(source.getName()); + } else { + viewportScale = 1.0f; + alphaTestOverride = null; + blendModeOverride = Optional.ofNullable(defaultBlendOverride); + bufferBlendInformations = Collections.emptyList(); + explicitFlips = ImmutableMap.of(); + } + + HashSet mipmappedBuffers = new HashSet<>(); + DispatchingDirectiveHolder directiveHolder = new DispatchingDirectiveHolder(); + + supportedRenderTargets.forEach(index -> { + BooleanConsumer mipmapHandler = shouldMipmap -> { + if (shouldMipmap) { + mipmappedBuffers.add(index); + } else { + mipmappedBuffers.remove(index); + } + }; + + directiveHolder.acceptConstBooleanDirective("colortex" + index + "MipmapEnabled", mipmapHandler); + + if (index < LEGACY_RENDER_TARGETS.size()) { + directiveHolder.acceptConstBooleanDirective(LEGACY_RENDER_TARGETS.get(index) + "MipmapEnabled", mipmapHandler); + } + }); + + source.getFragmentSource().map(ConstDirectiveParser::findDirectives).ifPresent(directives -> { + for (ConstDirectiveParser.ConstDirective directive : directives) { + directiveHolder.processDirective(directive); + } + }); + + this.mipmappedBuffers = ImmutableSet.copyOf(mipmappedBuffers); + } + + public ProgramDirectives withOverriddenDrawBuffers(int[] drawBuffersOverride) { + return new ProgramDirectives(drawBuffersOverride, viewportScale, alphaTestOverride, blendModeOverride, bufferBlendInformations, + mipmappedBuffers, explicitFlips); + } + + private static Optional findDrawbuffersDirective(Optional stageSource) { + return stageSource.flatMap(fragment -> CommentDirectiveParser.findDirective(fragment, CommentDirective.Type.DRAWBUFFERS)); + } + + private static Optional findRendertargetsDirective(Optional stageSource) { + return stageSource.flatMap(fragment -> CommentDirectiveParser.findDirective(fragment, CommentDirective.Type.RENDERTARGETS)); + } + + private static int[] parseDigits(char[] directiveChars) { + int[] buffers = new int[directiveChars.length]; + int index = 0; + + for (char buffer : directiveChars) { + buffers[index++] = Character.digit(buffer, 10); + } + + return buffers; + } + + private static int[] parseDigitList(String digitListString) { + return Arrays.stream(digitListString.split(",")) + .mapToInt(Integer::parseInt) + .toArray(); + } + + private static Optional getAppliedDirective(Optional optionalDrawbuffersDirective, Optional optionalRendertargetsDirective) { + if (optionalDrawbuffersDirective.isPresent() && optionalRendertargetsDirective.isPresent()) { + if (optionalDrawbuffersDirective.get().getLocation() > optionalRendertargetsDirective.get().getLocation()) { + return optionalDrawbuffersDirective; + } else { + return optionalRendertargetsDirective; + } + } else if (optionalDrawbuffersDirective.isPresent()) { + return optionalDrawbuffersDirective; + } else if (optionalRendertargetsDirective.isPresent()) { + return optionalRendertargetsDirective; + } else { + return Optional.empty(); + } + } + + public int[] getDrawBuffers() { + return drawBuffers; + } + + public float getViewportScale() { + return viewportScale; + } + + public Optional getAlphaTestOverride() { + return Optional.ofNullable(alphaTestOverride); + } + + public Optional getBlendModeOverride() { + return blendModeOverride; + } + + public List getBufferBlendOverrides() { + return bufferBlendInformations; + } + + public ImmutableSet getMipmappedBuffers() { + return mipmappedBuffers; + } + + public ImmutableMap getExplicitFlips() { + return explicitFlips; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ProgramFallbackResolver.java b/src/main/java/net/coderbot/iris/shaderpack/ProgramFallbackResolver.java new file mode 100644 index 000000000..aab07205f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ProgramFallbackResolver.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.shaderpack; + +import net.coderbot.iris.shaderpack.loading.ProgramId; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ProgramFallbackResolver { + private final ProgramSet programs; + private final Map cache; + + public ProgramFallbackResolver(ProgramSet programs) { + this.programs = programs; + this.cache = new HashMap<>(); + } + + public Optional resolve(ProgramId id) { + return Optional.ofNullable(resolveNullable(id)); + } + + @Nullable + public ProgramSource resolveNullable(ProgramId id) { + if (cache.containsKey(id)) { + return cache.get(id); + } + + ProgramSource source = programs.get(id).orElse(null); + + if (source == null) { + ProgramId fallback = id.getFallback().orElse(null); + + if (fallback != null) { + source = resolveNullable(fallback); + } + } + + cache.put(id, source); + return source; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ProgramSet.java b/src/main/java/net/coderbot/iris/shaderpack/ProgramSet.java new file mode 100644 index 000000000..ca6d01a7f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ProgramSet.java @@ -0,0 +1,453 @@ +package net.coderbot.iris.shaderpack; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.blending.BlendModeOverride; +import net.coderbot.iris.shaderpack.include.AbsolutePackPath; +import net.coderbot.iris.shaderpack.loading.ProgramId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class ProgramSet { + private final PackDirectives packDirectives; + + private final ProgramSource shadow; + private final ComputeSource[] shadowCompute; + + private final ProgramSource[] shadowcomp; + private final ComputeSource[][] shadowCompCompute; + private final ProgramSource[] prepare; + private final ComputeSource[][] prepareCompute; + + private final ProgramSource gbuffersBasic; + private final ProgramSource gbuffersLine; + private final ProgramSource gbuffersBeaconBeam; + private final ProgramSource gbuffersTextured; + private final ProgramSource gbuffersTexturedLit; + private final ProgramSource gbuffersTerrain; + private ProgramSource gbuffersDamagedBlock; + private final ProgramSource gbuffersSkyBasic; + private final ProgramSource gbuffersSkyTextured; + private final ProgramSource gbuffersClouds; + private final ProgramSource gbuffersWeather; + private final ProgramSource gbuffersEntities; + private final ProgramSource gbuffersEntitiesTrans; + private final ProgramSource gbuffersEntitiesGlowing; + private final ProgramSource gbuffersGlint; + private final ProgramSource gbuffersEntityEyes; + private final ProgramSource gbuffersBlock; + private final ProgramSource gbuffersHand; + + private final ProgramSource[] deferred; + private final ComputeSource[][] deferredCompute; + + private final ProgramSource gbuffersWater; + private final ProgramSource gbuffersHandWater; + + private final ProgramSource[] composite; + private final ComputeSource[][] compositeCompute; + private final ProgramSource compositeFinal; + private final ComputeSource[] finalCompute; + + + private final ShaderPack pack; + + public ProgramSet(AbsolutePackPath directory, Function sourceProvider, + ShaderProperties shaderProperties, ShaderPack pack) { + this.packDirectives = new PackDirectives(PackRenderTargetDirectives.BASELINE_SUPPORTED_RENDER_TARGETS, shaderProperties); + this.pack = pack; + + // Note: Ensure that blending is properly overridden during the shadow pass. By default, blending is disabled + // in the shadow pass. Shader packs expect this for colored shadows from stained glass and nether portals + // to work properly. + // + // Note: Enabling blending in the shadow pass results in weird results since translucency sorting happens + // relative to the player camera, not the shadow camera, so we can't rely on chunks being properly + // sorted in the shadow pass. + // + // - https://github.com/IrisShaders/Iris/issues/483 + // - https://github.com/IrisShaders/Iris/issues/987 + this.shadow = readProgramSource(directory, sourceProvider, "shadow", this, shaderProperties, + BlendModeOverride.OFF); + this.shadowCompute = readComputeArray(directory, sourceProvider, "shadow"); + + this.shadowcomp = readProgramArray(directory, sourceProvider, "shadowcomp", shaderProperties); + + this.shadowCompCompute = new ComputeSource[shadowcomp.length][]; + for (int i = 0; i < shadowcomp.length; i++) { + this.shadowCompCompute[i] = readComputeArray(directory, sourceProvider, "shadowcomp" + ((i == 0) ? "" : i)); + } + + this.prepare = readProgramArray(directory, sourceProvider, "prepare", shaderProperties); + this.prepareCompute = new ComputeSource[prepare.length][]; + for (int i = 0; i < prepare.length; i++) { + this.prepareCompute[i] = readComputeArray(directory, sourceProvider, "prepare" + ((i == 0) ? "" : i)); + } + + this.gbuffersBasic = readProgramSource(directory, sourceProvider, "gbuffers_basic", this, shaderProperties); + this.gbuffersLine = readProgramSource(directory, sourceProvider, "gbuffers_line", this, shaderProperties); + this.gbuffersBeaconBeam = readProgramSource(directory, sourceProvider, "gbuffers_beaconbeam", this, shaderProperties); + this.gbuffersTextured = readProgramSource(directory, sourceProvider, "gbuffers_textured", this, shaderProperties); + this.gbuffersTexturedLit = readProgramSource(directory, sourceProvider, "gbuffers_textured_lit", this, shaderProperties); + this.gbuffersTerrain = readProgramSource(directory, sourceProvider, "gbuffers_terrain", this, shaderProperties); + this.gbuffersDamagedBlock = readProgramSource(directory, sourceProvider, "gbuffers_damagedblock", this, shaderProperties); + this.gbuffersSkyBasic = readProgramSource(directory, sourceProvider, "gbuffers_skybasic", this, shaderProperties); + this.gbuffersSkyTextured = readProgramSource(directory, sourceProvider, "gbuffers_skytextured", this, shaderProperties); + this.gbuffersClouds = readProgramSource(directory, sourceProvider, "gbuffers_clouds", this, shaderProperties); + this.gbuffersWeather = readProgramSource(directory, sourceProvider, "gbuffers_weather", this, shaderProperties); + this.gbuffersEntities = readProgramSource(directory, sourceProvider, "gbuffers_entities", this, shaderProperties); + this.gbuffersEntitiesTrans = readProgramSource(directory, sourceProvider, "gbuffers_entities_translucent", this, shaderProperties); + this.gbuffersEntitiesGlowing = readProgramSource(directory, sourceProvider, "gbuffers_entities_glowing", this, shaderProperties); + this.gbuffersGlint = readProgramSource(directory, sourceProvider, "gbuffers_armor_glint", this, shaderProperties); + this.gbuffersEntityEyes = readProgramSource(directory, sourceProvider, "gbuffers_spidereyes", this, shaderProperties); + this.gbuffersBlock = readProgramSource(directory, sourceProvider, "gbuffers_block", this, shaderProperties); + this.gbuffersHand = readProgramSource(directory, sourceProvider, "gbuffers_hand", this, shaderProperties); + + this.deferred = readProgramArray(directory, sourceProvider, "deferred", shaderProperties); + this.deferredCompute = new ComputeSource[deferred.length][]; + for (int i = 0; i < deferred.length; i++) { + this.deferredCompute[i] = readComputeArray(directory, sourceProvider, "deferred" + ((i == 0) ? "" : i)); + } + + this.gbuffersWater = readProgramSource(directory, sourceProvider, "gbuffers_water", this, shaderProperties); + this.gbuffersHandWater = readProgramSource(directory, sourceProvider, "gbuffers_hand_water", this, shaderProperties); + + this.composite = readProgramArray(directory, sourceProvider, "composite", shaderProperties); + this.compositeCompute = new ComputeSource[composite.length][]; + for (int i = 0; i < deferred.length; i++) { + this.compositeCompute[i] = readComputeArray(directory, sourceProvider, "composite" + ((i == 0) ? "" : i)); + } + this.compositeFinal = readProgramSource(directory, sourceProvider, "final", this, shaderProperties); + this.finalCompute = readComputeArray(directory, sourceProvider, "final"); + + locateDirectives(); + + if (!gbuffersDamagedBlock.isValid()) { + // Special behavior inherited by OptiFine & Iris from old ShadersMod + // Presumably this was added before DRAWBUFFERS was a thing? Or just a hardcoded hacky fix for some + // shader packs - in any case, Sildurs Vibrant Shaders and other packs rely on it. + first(getGbuffersTerrain(), getGbuffersTexturedLit(), getGbuffersTextured(), getGbuffersBasic()).ifPresent(src -> { + ProgramDirectives overrideDirectives = src.getDirectives().withOverriddenDrawBuffers(new int[] { 0 }); + this.gbuffersDamagedBlock = src.withDirectiveOverride(overrideDirectives); + }); + } + } + + @SafeVarargs + private static Optional first(Optional... candidates) { + for (Optional candidate : candidates) { + if (candidate.isPresent()) { + return candidate; + } + } + + return Optional.empty(); + } + + private ProgramSource[] readProgramArray(AbsolutePackPath directory, + Function sourceProvider, String name, + ShaderProperties shaderProperties) { + ProgramSource[] programs = new ProgramSource[99]; + + for (int i = 0; i < programs.length; i++) { + String suffix = i == 0 ? "" : Integer.toString(i); + + programs[i] = readProgramSource(directory, sourceProvider, name + suffix, this, shaderProperties); + } + + return programs; + } + + private ComputeSource[] readComputeArray(AbsolutePackPath directory, + Function sourceProvider, String name) { + ComputeSource[] programs = new ComputeSource[27]; + + programs[0] = readComputeSource(directory, sourceProvider, name, this); + + for (char c = 'a'; c <= 'z'; ++c) { + String suffix = "_" + c; + + programs[c - 96] = readComputeSource(directory, sourceProvider, name + suffix, this); + + if (programs[c - 96] == null) { + break; + } + } + + return programs; + } + + private void locateDirectives() { + List programs = new ArrayList<>(); + List computes = new ArrayList<>(); + + programs.add(shadow); + programs.addAll(Arrays.asList(shadowcomp)); + programs.addAll(Arrays.asList(prepare)); + + programs.addAll (Arrays.asList( + gbuffersBasic, gbuffersBeaconBeam, gbuffersTextured, gbuffersTexturedLit, gbuffersTerrain, + gbuffersDamagedBlock, gbuffersSkyBasic, gbuffersSkyTextured, gbuffersClouds, gbuffersWeather, + gbuffersEntities, gbuffersEntitiesTrans, gbuffersEntitiesGlowing, gbuffersGlint, gbuffersEntityEyes, gbuffersBlock, + gbuffersHand + )); + + for (ComputeSource[] computeSources : compositeCompute) { + computes.addAll(Arrays.asList(computeSources)); + } + + for (ComputeSource[] computeSources : deferredCompute) { + computes.addAll(Arrays.asList(computeSources)); + } + + for (ComputeSource[] computeSources : prepareCompute) { + computes.addAll(Arrays.asList(computeSources)); + } + + for (ComputeSource[] computeSources : shadowCompCompute) { + computes.addAll(Arrays.asList(computeSources)); + } + + Collections.addAll(computes, finalCompute); + Collections.addAll(computes, shadowCompute); + + for (ComputeSource source : computes) { + if (source != null) { + source.getSource().map(ConstDirectiveParser::findDirectives).ifPresent(constDirectives -> { + for (ConstDirectiveParser.ConstDirective directive : constDirectives) { + if (directive.getType() == ConstDirectiveParser.Type.IVEC3 && directive.getKey().equals("workGroups")) { + ComputeDirectiveParser.setComputeWorkGroups(source, directive); + } else if (directive.getType() == ConstDirectiveParser.Type.VEC2 && directive.getKey().equals("workGroupsRender")) { + ComputeDirectiveParser.setComputeWorkGroupsRelative(source, directive); + } + } + }); + } + } + + programs.addAll(Arrays.asList(deferred)); + programs.add(gbuffersWater); + programs.add(gbuffersHandWater); + programs.addAll(Arrays.asList(composite)); + programs.add(compositeFinal); + + DispatchingDirectiveHolder packDirectiveHolder = new DispatchingDirectiveHolder(); + + packDirectives.acceptDirectivesFrom(packDirectiveHolder); + + for (ProgramSource source : programs) { + if (source == null) { + continue; + } + + source.getFragmentSource().map(ConstDirectiveParser::findDirectives).ifPresent(directives -> { + for (ConstDirectiveParser.ConstDirective directive : directives) { + packDirectiveHolder.processDirective(directive); + } + }); + } + + packDirectives.getRenderTargetDirectives().getRenderTargetSettings().forEach((index, settings) -> + Iris.logger.debug("Render target settings for colortex" + index + ": " + settings)); + } + + public Optional getShadow() { + return shadow.requireValid(); + } + + public ProgramSource[] getShadowComposite() { + return shadowcomp; + } + + public ProgramSource[] getPrepare() { + return prepare; + } + + public Optional getGbuffersBasic() { + return gbuffersBasic.requireValid(); + } + + public Optional getGbuffersBeaconBeam() { + return gbuffersBeaconBeam.requireValid(); + } + + public Optional getGbuffersTextured() { + return gbuffersTextured.requireValid(); + } + + public Optional getGbuffersTexturedLit() { + return gbuffersTexturedLit.requireValid(); + } + + public Optional getGbuffersTerrain() { + return gbuffersTerrain.requireValid(); + } + + public Optional getGbuffersDamagedBlock() { + return gbuffersDamagedBlock.requireValid(); + } + + public Optional getGbuffersSkyBasic() { + return gbuffersSkyBasic.requireValid(); + } + + public Optional getGbuffersSkyTextured() { + return gbuffersSkyTextured.requireValid(); + } + + public Optional getGbuffersClouds() { + return gbuffersClouds.requireValid(); + } + + public Optional getGbuffersWeather() { + return gbuffersWeather.requireValid(); + } + + public Optional getGbuffersEntities() { + return gbuffersEntities.requireValid(); + } + + public Optional getGbuffersEntitiesTrans() { + return gbuffersEntitiesTrans.requireValid(); + } + + public Optional getGbuffersEntitiesGlowing() { + return gbuffersEntitiesGlowing.requireValid(); + } + + public Optional getGbuffersGlint() { + return gbuffersGlint.requireValid(); + } + + public Optional getGbuffersEntityEyes() { + return gbuffersEntityEyes.requireValid(); + } + + public Optional getGbuffersBlock() { + return gbuffersBlock.requireValid(); + } + + public Optional getGbuffersHand() { + return gbuffersHand.requireValid(); + } + + public Optional get(ProgramId programId) { + return switch (programId) { + case Shadow -> getShadow(); + case Basic -> getGbuffersBasic(); + case Line -> gbuffersLine.requireValid(); + case Textured -> getGbuffersTextured(); + case TexturedLit -> getGbuffersTexturedLit(); + case SkyBasic -> getGbuffersSkyBasic(); + case SkyTextured -> getGbuffersSkyTextured(); + case Clouds -> getGbuffersClouds(); + case Terrain -> getGbuffersTerrain(); + case DamagedBlock -> getGbuffersDamagedBlock(); + case Block -> getGbuffersBlock(); + case BeaconBeam -> getGbuffersBeaconBeam(); + case Entities -> getGbuffersEntities(); + case EntitiesTrans -> getGbuffersEntitiesTrans(); + case EntitiesGlowing -> getGbuffersEntitiesGlowing(); + case ArmorGlint -> getGbuffersGlint(); + case SpiderEyes -> getGbuffersEntityEyes(); + case Hand -> getGbuffersHand(); + case Weather -> getGbuffersWeather(); + case Water -> getGbuffersWater(); + case HandWater -> getGbuffersHandWater(); + case Final -> getCompositeFinal(); + default -> Optional.empty(); + }; + } + + public ProgramSource[] getDeferred() { + return deferred; + } + + public Optional getGbuffersWater() { + return gbuffersWater.requireValid(); + } + + public Optional getGbuffersHandWater() { + return gbuffersHandWater.requireValid(); + } + + public ProgramSource[] getComposite() { + return composite; + } + + public Optional getCompositeFinal() { + return compositeFinal.requireValid(); + } + + public ComputeSource[] getShadowCompute() { + return shadowCompute; + } + + public ComputeSource[][] getShadowCompCompute() { + return shadowCompCompute; + } + + public ComputeSource[][] getPrepareCompute() { + return prepareCompute; + } + + public ComputeSource[][] getDeferredCompute() { + return deferredCompute; + } + + public ComputeSource[][] getCompositeCompute() { + return compositeCompute; + } + + public ComputeSource[] getFinalCompute() { + return finalCompute; + } + + public PackDirectives getPackDirectives() { + return packDirectives; + } + + public ShaderPack getPack() { + return pack; + } + + private static ProgramSource readProgramSource(AbsolutePackPath directory, + Function sourceProvider, String program, + ProgramSet programSet, ShaderProperties properties) { + return readProgramSource(directory, sourceProvider, program, programSet, properties, null); + } + + private static ProgramSource readProgramSource(AbsolutePackPath directory, + Function sourceProvider, String program, + ProgramSet programSet, ShaderProperties properties, + BlendModeOverride defaultBlendModeOverride) { + AbsolutePackPath vertexPath = directory.resolve(program + ".vsh"); + String vertexSource = sourceProvider.apply(vertexPath); + + AbsolutePackPath geometryPath = directory.resolve(program + ".gsh"); + String geometrySource = sourceProvider.apply(geometryPath); + + AbsolutePackPath fragmentPath = directory.resolve(program + ".fsh"); + String fragmentSource = sourceProvider.apply(fragmentPath); + + return new ProgramSource(program, vertexSource, geometrySource, fragmentSource, programSet, properties, + defaultBlendModeOverride); + } + + private static ComputeSource readComputeSource(AbsolutePackPath directory, + Function sourceProvider, String program, + ProgramSet programSet) { + AbsolutePackPath computePath = directory.resolve(program + ".csh"); + String computeSource = sourceProvider.apply(computePath); + + if (computeSource == null) { + return null; + } + + return new ComputeSource(program, computeSource, programSet); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ProgramSource.java b/src/main/java/net/coderbot/iris/shaderpack/ProgramSource.java new file mode 100644 index 000000000..9ef8500a7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ProgramSource.java @@ -0,0 +1,75 @@ +package net.coderbot.iris.shaderpack; + +import net.coderbot.iris.gl.blending.BlendModeOverride; + +import java.util.Optional; + +public class ProgramSource { + private final String name; + private final String vertexSource; + private final String geometrySource; + private final String fragmentSource; + private final ProgramDirectives directives; + private final ProgramSet parent; + + private ProgramSource(String name, String vertexSource, String geometrySource, String fragmentSource, + ProgramDirectives directives, ProgramSet parent) { + this.name = name; + this.vertexSource = vertexSource; + this.geometrySource = geometrySource; + this.fragmentSource = fragmentSource; + this.directives = directives; + this.parent = parent; + } + + public ProgramSource(String name, String vertexSource, String geometrySource, String fragmentSource, + ProgramSet parent, ShaderProperties properties, BlendModeOverride defaultBlendModeOverride) { + this.name = name; + this.vertexSource = vertexSource; + this.geometrySource = geometrySource; + this.fragmentSource = fragmentSource; + this.parent = parent; + this.directives = new ProgramDirectives(this, properties, + PackRenderTargetDirectives.BASELINE_SUPPORTED_RENDER_TARGETS, defaultBlendModeOverride); + } + + public ProgramSource withDirectiveOverride(ProgramDirectives overrideDirectives) { + return new ProgramSource(name, vertexSource, geometrySource, fragmentSource, overrideDirectives, parent); + } + + public String getName() { + return name; + } + + public Optional getVertexSource() { + return Optional.ofNullable(vertexSource); + } + + public Optional getGeometrySource() { + return Optional.ofNullable(geometrySource); + } + + public Optional getFragmentSource() { + return Optional.ofNullable(fragmentSource); + } + + public ProgramDirectives getDirectives() { + return this.directives; + } + + public ProgramSet getParent() { + return parent; + } + + public boolean isValid() { + return vertexSource != null && fragmentSource != null; + } + + public Optional requireValid() { + if (this.isValid()) { + return Optional.of(this); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java new file mode 100644 index 000000000..0371dff8a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java @@ -0,0 +1,377 @@ +package net.coderbot.iris.shaderpack; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.stream.JsonReader; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.features.FeatureFlags; +import net.coderbot.iris.shaderpack.include.AbsolutePackPath; +import net.coderbot.iris.shaderpack.include.IncludeGraph; +import net.coderbot.iris.shaderpack.include.IncludeProcessor; +import net.coderbot.iris.shaderpack.include.ShaderPackSourceNames; +import net.coderbot.iris.shaderpack.option.ProfileSet; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer; +import net.coderbot.iris.shaderpack.option.values.MutableOptionValues; +import net.coderbot.iris.shaderpack.option.values.OptionValues; +import net.coderbot.iris.shaderpack.preprocessor.JcppProcessor; +import net.coderbot.iris.shaderpack.texture.CustomTextureData; +import net.coderbot.iris.shaderpack.texture.TextureFilteringData; +import net.coderbot.iris.shaderpack.texture.TextureStage; +import net.irisshaders.iris.api.v0.IrisApi; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ShaderPack { + private static final Gson GSON = new Gson(); + + private final ProgramSet base; + @Nullable + private final ProgramSet overworld; + private final ProgramSet nether; + private final ProgramSet end; + + @Getter + private final IdMap idMap; + @Getter + private final LanguageMap languageMap; + @Getter + private final EnumMap> customTextureDataMap = new EnumMap<>(TextureStage.class); + private final CustomTextureData customNoiseTexture; + @Getter + private final ShaderPackOptions shaderPackOptions; + @Getter + private final OptionMenuContainer menuContainer; + + private final ProfileSet.ProfileResult profile; + private final String profileInfo; + + public ShaderPack(Path root, Iterable environmentDefines) throws IOException, IllegalStateException { + this(root, Collections.emptyMap(), environmentDefines); + } + + /** + * Reads a shader pack from the disk. + * + * @param root The path to the "shaders" directory within the shader pack. The created ShaderPack will not retain + * this path in any form; once the constructor exits, all disk I/O needed to load this shader pack will + * have completed, and there is no need to hold on to the path for that reason. + * @throws IOException if there are any IO errors during shader pack loading. + */ + public ShaderPack(Path root, Map changedConfigs, Iterable environmentDefines) throws IOException, IllegalStateException { + // A null path is not allowed. + Objects.requireNonNull(root); + + + ImmutableList.Builder starts = ImmutableList.builder(); + ImmutableList potentialFileNames = ShaderPackSourceNames.POTENTIAL_STARTS; + + ShaderPackSourceNames.findPresentSources(starts, root, AbsolutePackPath.fromAbsolutePath("/"), + potentialFileNames); + + boolean hasWorld0 = ShaderPackSourceNames.findPresentSources(starts, root, + AbsolutePackPath.fromAbsolutePath("/world0"), potentialFileNames); + + boolean hasNether = ShaderPackSourceNames.findPresentSources(starts, root, + AbsolutePackPath.fromAbsolutePath("/world-1"), potentialFileNames); + + boolean hasEnd = ShaderPackSourceNames.findPresentSources(starts, root, + AbsolutePackPath.fromAbsolutePath("/world1"), potentialFileNames); + + // Read all files and included files recursively + IncludeGraph graph = new IncludeGraph(root, starts.build()); + + if (!graph.getFailures().isEmpty()) { + graph.getFailures().forEach((path, error) -> { + Iris.logger.error("{}", error.toString()); + }); + + throw new IOException("Failed to resolve some #include directives, see previous messages for details"); + } + + this.languageMap = new LanguageMap(root.resolve("lang")); + + // Discover, merge, and apply shader pack options + this.shaderPackOptions = new ShaderPackOptions(graph, changedConfigs); + graph = this.shaderPackOptions.getIncludes(); + + Iterable finalEnvironmentDefines = environmentDefines; + ShaderProperties shaderProperties = loadProperties(root, "shaders.properties") + .map(source -> new ShaderProperties(source, shaderPackOptions, finalEnvironmentDefines)) + .orElseGet(ShaderProperties::empty); + + List invalidFlagList = shaderProperties.getRequiredFeatureFlags().stream().filter(FeatureFlags::isInvalid).map(FeatureFlags::getValue).collect(Collectors.toList()); + List invalidFeatureFlags = invalidFlagList.stream().map(FeatureFlags::getHumanReadableName).collect(Collectors.toList()); + + if (!invalidFeatureFlags.isEmpty()) { + // TODO: GUI +// if (Minecraft.getMinecraft().screen instanceof ShaderPackScreen) { +// Minecraft.getMinecraft().setScreen(new FeatureMissingErrorScreen(Minecraft.getMinecraft().screen, I18n.format("iris.unsupported.pack"), I18n.format("iris.unsupported.pack.description", FeatureFlags.getInvalidStatus(invalidFlagList), invalidFeatureFlags.stream() +// .collect(Collectors.joining(", ", ": ", "."))))); +// } + IrisApi.getInstance().getConfig().setShadersEnabledAndApply(false); + } + + List optionalFeatureFlags = shaderProperties.getOptionalFeatureFlags().stream().filter(flag -> !FeatureFlags.isInvalid(flag)).collect(Collectors.toList()); + + if (!optionalFeatureFlags.isEmpty()) { + List newEnvDefines = new ArrayList<>(); + environmentDefines.forEach(newEnvDefines::add); + optionalFeatureFlags.forEach(flag -> newEnvDefines.add(new StringPair("IRIS_FEATURE_" + flag, ""))); + environmentDefines = ImmutableList.copyOf(newEnvDefines); + } + + ProfileSet profiles = ProfileSet.fromTree(shaderProperties.getProfiles(), this.shaderPackOptions.getOptionSet()); + this.profile = profiles.scan(this.shaderPackOptions.getOptionSet(), this.shaderPackOptions.getOptionValues()); + + // Get programs that should be disabled from the detected profile + List disabledPrograms = new ArrayList<>(); + this.profile.current.ifPresent(profile -> disabledPrograms.addAll(profile.disabledPrograms)); + // Add programs that are disabled by shader options + shaderProperties.getConditionallyEnabledPrograms().forEach((program, shaderOption) -> { + if ("true".equals(shaderOption)) return; + + if ("false".equals(shaderOption) || !this.shaderPackOptions.getOptionValues().getBooleanValueOrDefault(shaderOption)) { + disabledPrograms.add(program); + } + }); + + this.menuContainer = new OptionMenuContainer(shaderProperties, this.shaderPackOptions, profiles); + + { + String profileName = getCurrentProfileName(); + OptionValues profileOptions = new MutableOptionValues( + this.shaderPackOptions.getOptionSet(), this.profile.current.map(p -> p.optionValues).orElse(new HashMap<>())); + + int userOptionsChanged = this.shaderPackOptions.getOptionValues().getOptionsChanged() - profileOptions.getOptionsChanged(); + + this.profileInfo = "Profile: " + profileName + " (+" + userOptionsChanged + " option" + (userOptionsChanged == 1 ? "" : "s") + " changed by user)"; + } + + Iris.logger.info(this.profileInfo); + + // Prepare our include processor + IncludeProcessor includeProcessor = new IncludeProcessor(graph); + + // Set up our source provider for creating ProgramSets + Iterable finalEnvironmentDefines1 = environmentDefines; + Function sourceProvider = (path) -> { + String pathString = path.getPathString(); + // Removes the first "/" in the path if present, and the file + // extension in order to represent the path as its program name + String programString = pathString.substring(pathString.indexOf("/") == 0 ? 1 : 0, pathString.lastIndexOf(".")); + + // Return an empty program source if the program is disabled by the current profile + if (disabledPrograms.contains(programString)) { + return null; + } + + ImmutableList lines = includeProcessor.getIncludedFile(path); + + if (lines == null) { + return null; + } + + StringBuilder builder = new StringBuilder(); + + for (String line : lines) { + builder.append(line); + builder.append('\n'); + } + + // Apply GLSL preprocessor to source, while making environment defines available. + // + // This uses similar techniques to the *.properties preprocessor to avoid actually putting + // #define statements in the actual source - instead, we tell the preprocessor about them + // directly. This removes one obstacle to accurate reporting of line numbers for errors, + // though there exist many more (such as relocating all #extension directives and similar things) + String source = builder.toString(); + source = JcppProcessor.glslPreprocessSource(source, finalEnvironmentDefines1); + + return source; + }; + + this.base = new ProgramSet(AbsolutePackPath.fromAbsolutePath("/"), sourceProvider, shaderProperties, this); + + this.overworld = loadOverrides(hasWorld0, AbsolutePackPath.fromAbsolutePath("/world0"), sourceProvider, shaderProperties, this); + this.nether = loadOverrides(hasNether, AbsolutePackPath.fromAbsolutePath("/world-1"), sourceProvider, shaderProperties, this); + this.end = loadOverrides(hasEnd, AbsolutePackPath.fromAbsolutePath("/world1"), sourceProvider, shaderProperties, this); + + this.idMap = new IdMap(root, shaderPackOptions, environmentDefines); + + customNoiseTexture = shaderProperties.getNoiseTexturePath().map(path -> { + try { + return readTexture(root, path); + } catch (IOException e) { + Iris.logger.error("Unable to read the custom noise texture at " + path, e); + + return null; + } + }).orElse(null); + + shaderProperties.getCustomTextures().forEach((textureStage, customTexturePropertiesMap) -> { + Object2ObjectMap innerCustomTextureDataMap = new Object2ObjectOpenHashMap<>(); + customTexturePropertiesMap.forEach((samplerName, path) -> { + try { + innerCustomTextureDataMap.put(samplerName, readTexture(root, path)); + } catch (IOException e) { + Iris.logger.error("Unable to read the custom texture at " + path, e); + } + }); + + customTextureDataMap.put(textureStage, innerCustomTextureDataMap); + }); + } + + private String getCurrentProfileName() { + return profile.current.map(p -> p.name).orElse("Custom"); + } + + public String getProfileInfo() { + return profileInfo; + } + + @Nullable + private static ProgramSet loadOverrides(boolean has, AbsolutePackPath path, Function sourceProvider, + ShaderProperties shaderProperties, ShaderPack pack) { + if (has) { + return new ProgramSet(path, sourceProvider, shaderProperties, pack); + } + + return null; + } + + // TODO: Copy-paste from IdMap, find a way to deduplicate this + private static Optional loadProperties(Path shaderPath, String name) { + String fileContents = readProperties(shaderPath, name); + if (fileContents == null) { + return Optional.empty(); + } + + return Optional.of(fileContents); + } + + // TODO: Implement raw texture data types + public CustomTextureData readTexture(Path root, String path) throws IOException { + CustomTextureData customTextureData; + if (path.contains(":")) { + String[] parts = path.split(":"); + + if (parts.length > 2) { + Iris.logger.warn("Resource location " + path + " contained more than two parts?"); + } + + if (parts[0].equals("minecraft") && (parts[1].equals("dynamic/lightmap_1") || parts[1].equals("dynamic/light_map_1"))) { + customTextureData = new CustomTextureData.LightmapMarker(); + } else { + customTextureData = new CustomTextureData.ResourceData(parts[0], parts[1]); + } + } else { + // TODO: Make sure the resulting path is within the shaderpack? + if (path.startsWith("/")) { + // NB: This does not guarantee the resulting path is in the shaderpack as a double slash could be used, + // this just fixes shaderpacks like Continuum 2.0.4 that use a leading slash in texture paths + path = path.substring(1); + } + + boolean blur = false; + boolean clamp = false; + + String mcMetaPath = path + ".mcmeta"; + Path mcMetaResolvedPath = root.resolve(mcMetaPath); + + if (Files.exists(mcMetaResolvedPath)) { + try { + JsonObject meta = loadMcMeta(mcMetaResolvedPath); + if (meta.get("texture") != null) { + if (meta.get("texture").getAsJsonObject().get("blur") != null) { + blur = meta.get("texture").getAsJsonObject().get("blur").getAsBoolean(); + } + if (meta.get("texture").getAsJsonObject().get("clamp") != null) { + clamp = meta.get("texture").getAsJsonObject().get("clamp").getAsBoolean(); + } + } + } catch (IOException e) { + Iris.logger.error("Unable to read the custom texture mcmeta at " + mcMetaPath + ", ignoring: " + e); + } + } + + byte[] content = Files.readAllBytes(root.resolve(path)); + + customTextureData = new CustomTextureData.PngData(new TextureFilteringData(blur, clamp), content); + } + return customTextureData; + } + + private JsonObject loadMcMeta(Path mcMetaPath) throws IOException, JsonParseException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(mcMetaPath), StandardCharsets.UTF_8))) { + JsonReader jsonReader = new JsonReader(reader); + return GSON.getAdapter(JsonObject.class).read(jsonReader); + } + } + + private static String readProperties(Path shaderPath, String name) { + try { + // Property files should be encoded in ISO_8859_1. + return new String(Files.readAllBytes(shaderPath.resolve(name)), StandardCharsets.ISO_8859_1); + } catch (NoSuchFileException e) { + Iris.logger.debug("An " + name + " file was not found in the current shaderpack"); + + return null; + } catch (IOException e) { + Iris.logger.error("An IOException occurred reading " + name + " from the current shaderpack", e); + + return null; + } + } + + public ProgramSet getProgramSet(DimensionId dimension) { + ProgramSet overrides = switch (dimension) { + case OVERWORLD -> overworld; + case NETHER -> nether; + case END -> end; + default -> throw new IllegalArgumentException("Unknown dimension " + dimension); + }; + + // NB: If a dimension overrides directory is present, none of the files from the parent directory are "merged" + // into the override. Rather, we act as if the overrides directory contains a completely different set of + // shader programs unrelated to that of the base shader pack. + // + // This makes sense because if base defined a composite pass and the override didn't, it would make it + // impossible to "un-define" the composite pass. It also removes a lot of complexity related to "merging" + // program sets. At the same time, this might be desired behavior by shader pack authors. It could make + // sense to bring it back as a configurable option, and have a more maintainable set of code backing it. + if (overrides != null) { + return overrides; + } else { + return base; + } + } + + public Optional getCustomNoiseTexture() { + return Optional.ofNullable(customNoiseTexture); + } + +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java b/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java new file mode 100644 index 000000000..fd5ed7257 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java @@ -0,0 +1,623 @@ +package net.coderbot.iris.shaderpack; + +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2FloatMap; +import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.blending.AlphaTest; +import net.coderbot.iris.gl.blending.AlphaTestFunction; +import net.coderbot.iris.gl.blending.AlphaTestOverride; +import net.coderbot.iris.gl.blending.BlendMode; +import net.coderbot.iris.gl.blending.BlendModeFunction; +import net.coderbot.iris.gl.blending.BlendModeOverride; +import net.coderbot.iris.gl.blending.BufferBlendInformation; +import net.coderbot.iris.gl.texture.TextureScaleOverride; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; +import net.coderbot.iris.shaderpack.preprocessor.PropertiesPreprocessor; +import net.coderbot.iris.shaderpack.texture.TextureStage; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * The parsed representation of the shaders.properties file. This class is not meant to be stored permanently, rather + * it merely exists as an intermediate step until we build up PackDirectives and ProgramDirectives objects from the + * values in here & the values parsed from shader source code. + */ +public class ShaderProperties { + private CloudSetting cloudSetting = CloudSetting.DEFAULT; + private OptionalBoolean oldHandLight = OptionalBoolean.DEFAULT; + private OptionalBoolean dynamicHandLight = OptionalBoolean.DEFAULT; + private OptionalBoolean oldLighting = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowTerrain = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowTranslucent = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowEntities = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowPlayer = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowBlockEntities = OptionalBoolean.DEFAULT; + private OptionalBoolean underwaterOverlay = OptionalBoolean.DEFAULT; + private OptionalBoolean sun = OptionalBoolean.DEFAULT; + private OptionalBoolean moon = OptionalBoolean.DEFAULT; + private OptionalBoolean vignette = OptionalBoolean.DEFAULT; + private OptionalBoolean backFaceSolid = OptionalBoolean.DEFAULT; + private OptionalBoolean backFaceCutout = OptionalBoolean.DEFAULT; + private OptionalBoolean backFaceCutoutMipped = OptionalBoolean.DEFAULT; + private OptionalBoolean backFaceTranslucent = OptionalBoolean.DEFAULT; + private OptionalBoolean rainDepth = OptionalBoolean.DEFAULT; + private OptionalBoolean concurrentCompute = OptionalBoolean.DEFAULT; + private OptionalBoolean beaconBeamDepth = OptionalBoolean.DEFAULT; + private OptionalBoolean separateAo = OptionalBoolean.DEFAULT; + private OptionalBoolean frustumCulling = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowCulling = OptionalBoolean.DEFAULT; + private OptionalBoolean shadowEnabled = OptionalBoolean.DEFAULT; + private OptionalBoolean particlesBeforeDeferred = OptionalBoolean.DEFAULT; + private OptionalBoolean prepareBeforeShadow = OptionalBoolean.DEFAULT; + private List sliderOptions = new ArrayList<>(); + private final Map> profiles = new LinkedHashMap<>(); + private List mainScreenOptions = null; + private final Map> subScreenOptions = new HashMap<>(); + private Integer mainScreenColumnCount = null; + private final Map subScreenColumnCount = new HashMap<>(); + // TODO: private Map optifineVersionRequirements; + // TODO: Parse custom uniforms / variables + private final Object2ObjectMap alphaTestOverrides = new Object2ObjectOpenHashMap<>(); + private final Object2FloatMap viewportScaleOverrides = new Object2FloatOpenHashMap<>(); + private final Object2ObjectMap textureScaleOverrides = new Object2ObjectOpenHashMap<>(); + private final Object2ObjectMap blendModeOverrides = new Object2ObjectOpenHashMap<>(); + private final Object2ObjectMap> bufferBlendOverrides = new Object2ObjectOpenHashMap<>(); + private final EnumMap> customTextures = new EnumMap<>(TextureStage.class); + private final Object2ObjectMap> explicitFlips = new Object2ObjectOpenHashMap<>(); + private String noiseTexturePath = null; + private Object2ObjectMap conditionallyEnabledPrograms = new Object2ObjectOpenHashMap<>(); + private List requiredFeatureFlags = new ArrayList<>(); + private List optionalFeatureFlags = new ArrayList<>(); + + private ShaderProperties() { + // empty + } + + // TODO: Is there a better solution than having ShaderPack pass a root path to ShaderProperties to be able to read textures? + public ShaderProperties(String contents, ShaderPackOptions shaderPackOptions, Iterable environmentDefines) { + String preprocessedContents = PropertiesPreprocessor.preprocessSource(contents, shaderPackOptions, environmentDefines); + + Properties preprocessed = new OrderBackedProperties(); + Properties original = new OrderBackedProperties(); + try { + preprocessed.load(new StringReader(preprocessedContents)); + original.load(new StringReader(contents)); + } catch (IOException e) { + Iris.logger.error("Error loading shaders.properties!", e); + } + + preprocessed.forEach((keyObject, valueObject) -> { + String key = (String) keyObject; + String value = (String) valueObject; + + if ("texture.noise".equals(key)) { + noiseTexturePath = value; + return; + } + + if ("clouds".equals(key)) { + if ("off".equals(value)) { + cloudSetting = CloudSetting.OFF; + } else if ("fast".equals(value)) { + cloudSetting = CloudSetting.FAST; + } else if ("fancy".equals(value)) { + cloudSetting = CloudSetting.FANCY; + } else { + Iris.logger.error("Unrecognized clouds setting: " + value); + } + } + + handleBooleanDirective(key, value, "oldHandLight", bool -> oldHandLight = bool); + handleBooleanDirective(key, value, "dynamicHandLight", bool -> dynamicHandLight = bool); + handleBooleanDirective(key, value, "oldLighting", bool -> oldLighting = bool); + handleBooleanDirective(key, value, "shadowTerrain", bool -> shadowTerrain = bool); + handleBooleanDirective(key, value, "shadowTranslucent", bool -> shadowTranslucent = bool); + handleBooleanDirective(key, value, "shadowEntities", bool -> shadowEntities = bool); + handleBooleanDirective(key, value, "shadowPlayer", bool -> shadowPlayer = bool); + handleBooleanDirective(key, value, "shadowBlockEntities", bool -> shadowBlockEntities = bool); + handleBooleanDirective(key, value, "underwaterOverlay", bool -> underwaterOverlay = bool); + handleBooleanDirective(key, value, "sun", bool -> sun = bool); + handleBooleanDirective(key, value, "moon", bool -> moon = bool); + handleBooleanDirective(key, value, "vignette", bool -> vignette = bool); + handleBooleanDirective(key, value, "backFace.solid", bool -> backFaceSolid = bool); + handleBooleanDirective(key, value, "backFace.cutout", bool -> backFaceCutout = bool); + handleBooleanDirective(key, value, "backFace.cutoutMipped", bool -> backFaceCutoutMipped = bool); + handleBooleanDirective(key, value, "backFace.translucent", bool -> backFaceTranslucent = bool); + handleBooleanDirective(key, value, "rain.depth", bool -> rainDepth = bool); + handleBooleanDirective(key, value, "allowConcurrentCompute", bool -> concurrentCompute = bool); + handleBooleanDirective(key, value, "beacon.beam.depth", bool -> beaconBeamDepth = bool); + handleBooleanDirective(key, value, "separateAo", bool -> separateAo = bool); + handleBooleanDirective(key, value, "frustum.culling", bool -> frustumCulling = bool); + handleBooleanDirective(key, value, "shadow.culling", bool -> shadowCulling = bool); + handleBooleanDirective(key, value, "particles.before.deferred", bool -> particlesBeforeDeferred = bool); + handleBooleanDirective(key, value, "prepareBeforeShadow", bool -> prepareBeforeShadow = bool); + + // TODO: Min optifine versions, shader options layout / appearance / profiles + // TODO: Custom uniforms + + handlePassDirective("scale.", key, value, pass -> { + float scale; + + try { + scale = Float.parseFloat(value); + } catch (NumberFormatException e) { + Iris.logger.error("Unable to parse scale directive for " + pass + ": " + value, e); + return; + } + + viewportScaleOverrides.put(pass, scale); + }); + + handlePassDirective("size.buffer.", key, value, pass -> { + String[] parts = value.split(" "); + + if (parts.length != 2) { + Iris.logger.error("Unable to parse size.buffer directive for " + pass + ": " + value); + return; + } + + textureScaleOverrides.put(pass, new TextureScaleOverride(parts[0], parts[1])); + }); + + handlePassDirective("alphaTest.", key, value, pass -> { + if ("off".equals(value)) { + alphaTestOverrides.put(pass, AlphaTestOverride.OFF); + return; + } + + String[] parts = value.split(" "); + + if (parts.length > 2) { + Iris.logger.warn("Weird alpha test directive for " + pass + " contains more parts than we expected: " + value); + } else if (parts.length < 2) { + Iris.logger.error("Invalid alpha test directive for " + pass + ": " + value); + return; + } + + Optional function = AlphaTestFunction.fromString(parts[0]); + + if (!function.isPresent()) { + Iris.logger.error("Unable to parse alpha test directive for " + pass + ", unknown alpha test function " + parts[0] + ": " + value); + return; + } + + float reference; + + try { + reference = Float.parseFloat(parts[1]); + } catch (NumberFormatException e) { + Iris.logger.error("Unable to parse alpha test directive for " + pass + ": " + value, e); + return; + } + + alphaTestOverrides.put(pass, new AlphaTestOverride(new AlphaTest(function.get(), reference))); + }); + + handlePassDirective("blend.", key, value, pass -> { + if (pass.contains(".")) { + + if (!IrisRenderSystem.supportsBufferBlending()) { + throw new RuntimeException("Buffer blending is not supported on this platform, however it was attempted to be used!"); + } + + String[] parts = pass.split("\\."); + int index = PackRenderTargetDirectives.LEGACY_RENDER_TARGETS.indexOf(parts[1]); + + if (index == -1 && parts[1].startsWith("colortex")) { + String id = parts[1].substring("colortex".length()); + + try { + index = Integer.parseInt(id); + } catch (NumberFormatException e) { + throw new RuntimeException("Failed to parse buffer blend!", e); + } + } + + if (index == -1) { + throw new RuntimeException("Failed to parse buffer blend! index = " + index); + } + + if ("off".equals(value)) { + bufferBlendOverrides.computeIfAbsent(parts[0], list -> new ArrayList<>()).add(new BufferBlendInformation(index, null)); + return; + } + + String[] modeArray = value.split(" "); + int[] modes = new int[modeArray.length]; + + int i = 0; + for (String modeName : modeArray) { + modes[i] = BlendModeFunction.fromString(modeName).get().getGlId(); + i++; + } + + bufferBlendOverrides.computeIfAbsent(parts[0], list -> new ArrayList<>()).add(new BufferBlendInformation(index, new BlendMode(modes[0], modes[1], modes[2], modes[3]))); + + return; + } + + if ("off".equals(value)) { + blendModeOverrides.put(pass, BlendModeOverride.OFF); + return; + } + + String[] modeArray = value.split(" "); + int[] modes = new int[modeArray.length]; + + int i = 0; + for (String modeName : modeArray) { + modes[i] = BlendModeFunction.fromString(modeName).get().getGlId(); + i++; + } + + blendModeOverrides.put(pass, new BlendModeOverride(new BlendMode(modes[0], modes[1], modes[2], modes[3]))); + }); + + handleProgramEnabledDirective("program.", key, value, program -> { + conditionallyEnabledPrograms.put(program, value); + }); + + handleTwoArgDirective("texture.", key, value, (stageName, samplerName) -> { + String[] parts = value.split(" "); + + // TODO: Support raw textures + if (parts.length > 1) { + Iris.logger.warn("Custom texture directive for stage " + stageName + ", sampler " + samplerName + " contains more parts than we expected: " + value); + return; + } + + Optional optionalTextureStage = TextureStage.parse(stageName); + + if (!optionalTextureStage.isPresent()) { + Iris.logger.warn("Unknown texture stage " + "\"" + stageName + "\"," + " ignoring custom texture directive for " + key); + return; + } + + TextureStage stage = optionalTextureStage.get(); + + customTextures.computeIfAbsent(stage, _stage -> new Object2ObjectOpenHashMap<>()) + .put(samplerName, value); + }); + + handleTwoArgDirective("flip.", key, value, (pass, buffer) -> { + handleBooleanValue(key, value, shouldFlip -> { + explicitFlips.computeIfAbsent(pass, _pass -> new Object2BooleanOpenHashMap<>()) + .put(buffer, shouldFlip); + }); + }); + + + handleWhitespacedListDirective(key, value, "iris.features.required", options -> requiredFeatureFlags = options); + handleWhitespacedListDirective(key, value, "iris.features.optional", options -> optionalFeatureFlags = options); + + // TODO: Buffer size directives + // TODO: Conditional program enabling directives + }); + + // We need to use a non-preprocessed property file here since we don't want any weird preprocessor changes to be applied to the screen/value layout. + original.forEach((keyObject, valueObject) -> { + String key = (String) keyObject; + String value = (String) valueObject; + + // Defining "sliders" multiple times in the properties file will only result in + // the last definition being used, should be tested if behavior matches OptiFine + handleWhitespacedListDirective(key, value, "sliders", sliders -> sliderOptions = sliders); + handlePrefixedWhitespacedListDirective("profile.", key, value, profiles::put); + + if (handleIntDirective(key, value, "screen.columns", columns -> mainScreenColumnCount = columns)) { + return; + } + + if (handleAffixedIntDirective("screen.", ".columns", key, value, subScreenColumnCount::put)) { + return; + } + + handleWhitespacedListDirective(key, value, "screen", options -> mainScreenOptions = options); + handlePrefixedWhitespacedListDirective("screen.", key, value, subScreenOptions::put); + }); + } + + private static void handleBooleanValue(String key, String value, BooleanConsumer handler) { + if ("true".equals(value)) { + handler.accept(true); + } else if ("false".equals(value)) { + handler.accept(false); + } else { + Iris.logger.warn("Unexpected value for boolean key " + key + " in shaders.properties: got " + value + ", but expected either true or false"); + } + } + + private static void handleBooleanDirective(String key, String value, String expectedKey, Consumer handler) { + if (!expectedKey.equals(key)) { + return; + } + + if ("true".equals(value)) { + handler.accept(OptionalBoolean.TRUE); + } else if ("false".equals(value)) { + handler.accept(OptionalBoolean.FALSE); + } else { + Iris.logger.warn("Unexpected value for boolean key " + key + " in shaders.properties: got " + value + ", but expected either true or false"); + } + } + + private static boolean handleIntDirective(String key, String value, String expectedKey, Consumer handler) { + if (!expectedKey.equals(key)) { + return false; + } + + try { + int result = Integer.parseInt(value); + + handler.accept(result); + } catch (NumberFormatException nex) { + Iris.logger.warn("Unexpected value for integer key " + key + " in shaders.properties: got " + value + ", but expected an integer"); + } + + return true; + } + + private static boolean handleAffixedIntDirective(String prefix, String suffix, String key, String value, BiConsumer handler) { + if (key.startsWith(prefix) && key.endsWith(suffix)) { + int substrBegin = prefix.length(); + int substrEnd = key.length() - suffix.length(); + + if (substrEnd <= substrBegin) { + return false; + } + + String affixStrippedKey = key.substring(substrBegin, substrEnd); + + try { + int result = Integer.parseInt(value); + + handler.accept(affixStrippedKey, result); + } catch (NumberFormatException nex) { + Iris.logger.warn("Unexpected value for integer key " + key + " in shaders.properties: got " + value + ", but expected an integer"); + } + + return true; + } + + return false; + } + + private static void handlePassDirective(String prefix, String key, String value, Consumer handler) { + if (key.startsWith(prefix)) { + String pass = key.substring(prefix.length()); + + handler.accept(pass); + } + } + + private static void handleProgramEnabledDirective(String prefix, String key, String value, Consumer handler) { + if (key.startsWith(prefix)) { + String program = key.substring(prefix.length(), key.indexOf(".", prefix.length())); + + handler.accept(program); + } + } + + private static void handleWhitespacedListDirective(String key, String value, String expectedKey, Consumer> handler) { + if (!expectedKey.equals(key)) { + return; + } + + String[] elements = value.split(" +"); + + handler.accept(Arrays.asList(elements)); + } + + private static void handlePrefixedWhitespacedListDirective(String prefix, String key, String value, BiConsumer> handler) { + if (key.startsWith(prefix)) { + String prefixStrippedKey = key.substring(prefix.length()); + String[] elements = value.split(" +"); + + handler.accept(prefixStrippedKey, Arrays.asList(elements)); + } + } + + private static void handleTwoArgDirective(String prefix, String key, String value, BiConsumer handler) { + if (key.startsWith(prefix)) { + int endOfPassIndex = key.indexOf(".", prefix.length()); + String stage = key.substring(prefix.length(), endOfPassIndex); + String sampler = key.substring(endOfPassIndex + 1); + + handler.accept(stage, sampler); + } + } + + public static ShaderProperties empty() { + return new ShaderProperties(); + } + + public CloudSetting getCloudSetting() { + return cloudSetting; + } + + public OptionalBoolean getOldHandLight() { + return oldHandLight; + } + + public OptionalBoolean getDynamicHandLight() { + return dynamicHandLight; + } + + public OptionalBoolean getOldLighting() { + return oldLighting; + } + + public OptionalBoolean getShadowTerrain() { + return shadowTerrain; + } + + public OptionalBoolean getShadowTranslucent() { + return shadowTranslucent; + } + + public OptionalBoolean getShadowEntities() { + return shadowEntities; + } + + public OptionalBoolean getShadowPlayer() { + return shadowPlayer; + } + + public OptionalBoolean getShadowBlockEntities() { + return shadowBlockEntities; + } + + public OptionalBoolean getUnderwaterOverlay() { + return underwaterOverlay; + } + + public OptionalBoolean getSun() { + return sun; + } + + public OptionalBoolean getMoon() { + return moon; + } + + public OptionalBoolean getVignette() { + return vignette; + } + + public OptionalBoolean getBackFaceSolid() { + return backFaceSolid; + } + + public OptionalBoolean getBackFaceCutout() { + return backFaceCutout; + } + + public OptionalBoolean getBackFaceCutoutMipped() { + return backFaceCutoutMipped; + } + + public OptionalBoolean getBackFaceTranslucent() { + return backFaceTranslucent; + } + + public OptionalBoolean getRainDepth() { + return rainDepth; + } + + public OptionalBoolean getBeaconBeamDepth() { + return beaconBeamDepth; + } + + public OptionalBoolean getSeparateAo() { + return separateAo; + } + + public OptionalBoolean getFrustumCulling() { + return frustumCulling; + } + + public OptionalBoolean getShadowCulling() { + return shadowCulling; + } + + public OptionalBoolean getShadowEnabled() { + return shadowEnabled; + } + + public OptionalBoolean getParticlesBeforeDeferred() { + return particlesBeforeDeferred; + } + + public OptionalBoolean getConcurrentCompute() { + return concurrentCompute; + } + + public OptionalBoolean getPrepareBeforeShadow() { + return prepareBeforeShadow; + } + + public Object2ObjectMap getAlphaTestOverrides() { + return alphaTestOverrides; + } + + public Object2FloatMap getViewportScaleOverrides() { + return viewportScaleOverrides; + } + + public Object2ObjectMap getTextureScaleOverrides() { + return textureScaleOverrides; + } + + public Object2ObjectMap getBlendModeOverrides() { + return blendModeOverrides; + } + + public Object2ObjectMap> getBufferBlendOverrides() { + return bufferBlendOverrides; + } + + public EnumMap> getCustomTextures() { + return customTextures; + } + + public Optional getNoiseTexturePath() { + return Optional.ofNullable(noiseTexturePath); + } + + public Object2ObjectMap getConditionallyEnabledPrograms() { + return conditionallyEnabledPrograms; + } + + public List getSliderOptions() { + return sliderOptions; + } + + public Map> getProfiles() { + return profiles; + } + + public Optional> getMainScreenOptions() { + return Optional.ofNullable(mainScreenOptions); + } + + public Map> getSubScreenOptions() { + return subScreenOptions; + } + + public Optional getMainScreenColumnCount() { + return Optional.ofNullable(mainScreenColumnCount); + } + + public Map getSubScreenColumnCount() { + return subScreenColumnCount; + } + + public Object2ObjectMap> getExplicitFlips() { + return explicitFlips; + } + + public List getRequiredFeatureFlags() { + return requiredFeatureFlags; + } + + public List getOptionalFeatureFlags() { + return optionalFeatureFlags; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/StringPair.java b/src/main/java/net/coderbot/iris/shaderpack/StringPair.java new file mode 100644 index 000000000..dc3fbc376 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/StringPair.java @@ -0,0 +1,28 @@ +package net.coderbot.iris.shaderpack; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * An absurdly simple class for storing pairs of strings because Java lacks pair / tuple types. + */ +public class StringPair { + private final String key; + private final String value; + + public StringPair(@NotNull String key, @NotNull String value) { + this.key = Objects.requireNonNull(key); + this.value = Objects.requireNonNull(value); + } + + @NotNull + public String getKey() { + return key; + } + + @NotNull + public String getValue() { + return value; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/discovery/ShaderpackDirectoryManager.java b/src/main/java/net/coderbot/iris/shaderpack/discovery/ShaderpackDirectoryManager.java new file mode 100644 index 000000000..1e1bbf1f0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/discovery/ShaderpackDirectoryManager.java @@ -0,0 +1,103 @@ +package net.coderbot.iris.shaderpack.discovery; + +import net.coderbot.iris.Iris; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Comparator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ShaderpackDirectoryManager { + private final Path root; + + public ShaderpackDirectoryManager(Path root) { + this.root = root; + } + + public void copyPackIntoDirectory(String name, Path source) throws IOException { + Path target = Iris.getShaderpacksDirectory().resolve(name); + + // Copy the pack file into the shaderpacks folder. + Files.copy(source, target); + // Zip or other archive files will be copied without issue, + // however normal folders will require additional handling below. + + // Manually copy the contents of the pack if it is a folder + if (Files.isDirectory(source)) { + // Use for loops instead of forEach due to createDirectory throwing an IOException + // which requires additional handling when used in a lambda + + // Copy all sub folders, collected as a list in order to prevent issues with non-ordered sets + try (Stream stream = Files.walk(source)) { + for (Path p : stream.filter(Files::isDirectory).collect(Collectors.toList())) { + Path folder = source.relativize(p); + + if (Files.exists(folder)) { + continue; + } + + Files.createDirectory(target.resolve(folder)); + } + } + + // Copy all non-folder files + try (Stream stream = Files.walk(source)) { + for (Path p : stream.filter(p -> !Files.isDirectory(p)).collect(Collectors.toSet())) { + Path file = source.relativize(p); + + Files.copy(p, target.resolve(file)); + } + } + } + } + + public Collection enumerate() throws IOException { + // Make sure the list is sorted since not all OSes sort the list of files in the directory. + // Case-insensitive sorting is the most intuitive for the user, but we then sort naturally + // afterwards so that we don't alternate cases weirdly in the sorted list. + // + // We also ignore chat formatting characters when sorting - some shader packs include chat + // formatting in the file name so that they have fancy text when displayed in the shaders list. + Comparator baseComparator = String.CASE_INSENSITIVE_ORDER.thenComparing(Comparator.naturalOrder()); + Comparator comparator = (a, b) -> { + a = removeFormatting(a); + b = removeFormatting(b); + + return baseComparator.compare(a, b); + }; + + try (Stream list = Files.list(root)) { + return list.filter(Iris::isValidShaderpack) + .map(path -> path.getFileName().toString()) + .sorted(comparator).collect(Collectors.toList()); + } + } + + /** + * Straightforward method to use section-sign based chat formatting from a String + */ + private static String removeFormatting(String formatted) { + char[] original = formatted.toCharArray(); + char[] cleaned = new char[original.length]; + int c = 0; + + for (int i = 0; i < original.length; i++) { + // check if it's a section sign + if (original[i] == '\u00a7') { + i++; + } else { + cleaned[c++] = original[i]; + } + } + + return new String(cleaned, 0, c); + } + + public URI getDirectoryUri() { + return root.toUri(); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/error/RusticError.java b/src/main/java/net/coderbot/iris/shaderpack/error/RusticError.java new file mode 100644 index 000000000..4e188d76f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/error/RusticError.java @@ -0,0 +1,55 @@ +package net.coderbot.iris.shaderpack.error; + +import org.apache.commons.lang3.StringUtils; + +public class RusticError { + private final String severity; + private final String message; + private final String detailMessage; + private final String file; + private final int lineNumber; + private final String badLine; + + public RusticError(String severity, String message, String detailMessage, String file, int lineNumber, String badLine) { + this.severity = severity; + this.message = message; + this.detailMessage = detailMessage; + this.file = file; + this.lineNumber = lineNumber; + this.badLine = badLine; + } + + public String getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + public String getDetailMessage() { + return detailMessage; + } + + public String getFile() { + return file; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getBadLine() { + return badLine; + } + + @Override + public String toString() { + return severity + ": " + message + "\n" + + " --> " + file + ":" + lineNumber + "\n" + + " |\n" + + " | " + badLine + "\n" + + " | " + StringUtils.repeat('^', badLine.length()) + " " + detailMessage + "\n" + + " |"; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/include/AbsolutePackPath.java b/src/main/java/net/coderbot/iris/shaderpack/include/AbsolutePackPath.java new file mode 100644 index 000000000..5ab3183ec --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/include/AbsolutePackPath.java @@ -0,0 +1,113 @@ +package net.coderbot.iris.shaderpack.include; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +// TODO: Write tests for this +public class AbsolutePackPath { + private final String path; + + private AbsolutePackPath(String absolute) { + this.path = absolute; + } + + public static AbsolutePackPath fromAbsolutePath(String absolutePath) { + return new AbsolutePackPath(normalizeAbsolutePath(absolutePath)); + } + + public Optional parent() { + if (path.equals("/")) { + return Optional.empty(); + } + + int lastSlash = path.lastIndexOf('/'); + + return Optional.of(new AbsolutePackPath(path.substring(0, lastSlash))); + } + + public AbsolutePackPath resolve(String path) { + if (path.startsWith("/")) { + return fromAbsolutePath(path); + } + + String merged; + + if (!this.path.endsWith("/") & !path.startsWith("/")) { + merged = this.path + "/" + path; + } else { + merged = this.path + path; + } + + return fromAbsolutePath(merged); + } + + public Path resolved(Path root) { + if (path.equals("/")) { + return root; + } + + return root.resolve(path.substring(1)); + } + + private static String normalizeAbsolutePath(String path) { + if (!path.startsWith("/")) { + throw new IllegalArgumentException("Not an absolute path: " + path); + } + + String[] segments = path.split(Pattern.quote("/")); + List parsedSegments = new ArrayList<>(); + + for (String segment : segments) { + if (segment.isEmpty() || segment.equals(".")) { + continue; + } + + if (segment.equals("..")) { + if (!parsedSegments.isEmpty()) { + parsedSegments.remove(parsedSegments.size() - 1); + } + } else { + parsedSegments.add(segment); + } + } + + if (parsedSegments.isEmpty()) { + return "/"; + } + + StringBuilder normalized = new StringBuilder(); + + for (String segment : parsedSegments) { + normalized.append('/'); + normalized.append(segment); + } + + return normalized.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbsolutePackPath that = (AbsolutePackPath) o; + return Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(path); + } + + @Override + public String toString() { + return "AbsolutePackPath {" + getPathString() + "}"; + } + + public String getPathString() { + return path; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/include/FileNode.java b/src/main/java/net/coderbot/iris/shaderpack/include/FileNode.java new file mode 100644 index 000000000..8a87fc044 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/include/FileNode.java @@ -0,0 +1,98 @@ +package net.coderbot.iris.shaderpack.include; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.shaderpack.transform.line.LineTransform; + +import java.util.Objects; + +public class FileNode { + private final AbsolutePackPath path; + private final ImmutableList lines; + private final ImmutableMap includes; + + // NB: The caller is responsible for ensuring that the includes map + // is in sync with the lines list. + private FileNode(AbsolutePackPath path, ImmutableList lines, + ImmutableMap includes) { + this.path = path; + this.lines = lines; + this.includes = includes; + } + + public FileNode(AbsolutePackPath path, ImmutableList lines) { + this.path = path; + this.lines = lines; + + AbsolutePackPath currentDirectory = path.parent().orElseThrow( + () -> new IllegalArgumentException("Not a valid shader file name: " + path)); + + this.includes = findIncludes(currentDirectory, lines); + } + + public AbsolutePackPath getPath() { + return path; + } + + public ImmutableList getLines() { + return lines; + } + + public ImmutableMap getIncludes() { + return includes; + } + + public FileNode map(LineTransform transform) { + ImmutableList.Builder newLines = ImmutableList.builder(); + int index = 0; + + for (String line : lines) { + String transformedLine = transform.transform(index, line); + + if (includes.containsKey(index)) { + if (!Objects.equals(line, transformedLine)) { + throw new IllegalStateException("Attempted to modify an #include line in LineTransform."); + } + } + + newLines.add(transformedLine); + index += 1; + } + + return new FileNode(path, newLines.build(), includes); + } + + private static ImmutableMap findIncludes(AbsolutePackPath currentDirectory, + ImmutableList lines) { + ImmutableMap.Builder foundIncludes = ImmutableMap.builder(); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).trim(); + + if (!line.startsWith("#include")) { + continue; + } + + // Remove the "#include " part so that we just have the file path + String target = line.substring("#include ".length()).trim(); + + // Remove quotes if they're present + // All include directives should have quotes, but I'm not sure whether they're required to. + // TODO: Check if quotes are required, and don't permit mismatched quotes + // TODO: This shouldn't be accepted: + // #include "test.glsl + // #include test.glsl" + if (target.startsWith("\"")) { + target = target.substring(1); + } + + if (target.endsWith("\"")) { + target = target.substring(0, target.length() - 1); + } + + foundIncludes.put(i, currentDirectory.resolve(target)); + } + + return foundIncludes.build(); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/include/IncludeGraph.java b/src/main/java/net/coderbot/iris/shaderpack/include/IncludeGraph.java new file mode 100644 index 000000000..e1188115b --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/include/IncludeGraph.java @@ -0,0 +1,260 @@ +package net.coderbot.iris.shaderpack.include; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.error.RusticError; +import net.coderbot.iris.shaderpack.transform.line.LineTransform; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +/** + * A directed graph data structure that holds the loaded source of all shader programs + * and the files included by each source file. Each node / vertex in the graph + * corresponds to a single file in the shader pack, and each edge / connection + * corresponds to an {@code #include} directive on a given line. + * + *

Using a proper graph representation allows us to apply existing and + * efficient algorithms with well-known properties to various tasks and + * transformations necessary during shader pack loading. We receive a number of + * immediate benefits from using a graph-based representation:

+ * + *
    + *
  • Each file is read exactly one time, and it is only necessary to + * parse #include directives from a file once. This ensures efficient + * IO. + *
  • + *
  • Deferring the processing of inclusions allows transformers that only + * need to replace single lines at a time to operate more efficiently, + * avoiding processing lines duplicated across many files more than + * necessary. + *
  • + *
  • As a result, our shader configuration system is able to process and + * apply options much more efficiently than a naive one operating on + * included files only, allowing many operations to scale much more + * nicely, especially in the common case of shader pack authors having + * a single large settings file defining every config option that is + * then included in every shader program in the pack. + *
  • + *
  • Deferred processing of inclusions also allows the shader pack loader + * to reason about cyclic inclusions, allowing us to remove the + * arbitrary file include depth limit, and avoid stack overflows due to + * infinite recursion that a naive implementation might be subject to. + *
  • + *
+ */ +public class IncludeGraph { + private final ImmutableMap nodes; + private final ImmutableMap failures; + + private IncludeGraph(ImmutableMap nodes, + ImmutableMap failures) { + this.nodes = nodes; + this.failures = failures; + } + + public IncludeGraph(Path root, ImmutableList startingPaths) { + Map cameFrom = new HashMap<>(); + Map lineNumberInclude = new HashMap<>(); + + Map nodes = new HashMap<>(); + Map failures = new HashMap<>(); + + List queue = new ArrayList<>(startingPaths); + Set seen = new HashSet<>(startingPaths); + + while (!queue.isEmpty()) { + AbsolutePackPath next = queue.remove(queue.size() - 1); + + String source; + + try { + source = readFile(next.resolved(root)); + } catch (IOException e) { + AbsolutePackPath src = cameFrom.get(next); + + if (src == null) { + throw new RuntimeException("unexpected error: failed to read " + next.getPathString(), e); + } + + String topLevelMessage; + String detailMessage; + + if (e instanceof NoSuchFileException) { + topLevelMessage = "failed to resolve #include directive"; + detailMessage = "file not found"; + } else { + topLevelMessage = "unexpected I/O error while resolving #include directive: " + e; + detailMessage = "IO error"; + } + + String badLine = nodes.get(src).getLines().get(lineNumberInclude.get(next)).trim(); + + RusticError topLevelError = new RusticError("error", topLevelMessage, detailMessage, src.getPathString(), + lineNumberInclude.get(next) + 1, badLine); + + failures.put(next, topLevelError); + + continue; + } + + ImmutableList lines = ImmutableList.copyOf(source.split("\\R")); + + FileNode node = new FileNode(next, lines); + boolean selfInclude = false; + + for (Map.Entry include : node.getIncludes().entrySet()) { + int line = include.getKey(); + AbsolutePackPath included = include.getValue(); + + if (next.equals(included)) { + selfInclude = true; + failures.put(next, new RusticError("error", "trivial #include cycle detected", + "file includes itself", next.getPathString(), line + 1, lines.get(line))); + + break; + } else if (!seen.contains(included)) { + queue.add(included); + seen.add(included); + cameFrom.put(included, next); + lineNumberInclude.put(included, line); + } + } + + if (!selfInclude) { + nodes.put(next, node); + } + } + + this.nodes = ImmutableMap.copyOf(nodes); + this.failures = ImmutableMap.copyOf(failures); + + detectCycle(); + } + + private void detectCycle() { + List cycle = new ArrayList<>(); + Set visited = new HashSet<>(); + + for (AbsolutePackPath start : nodes.keySet()) { + if (exploreForCycles(start, cycle, visited)) { + AbsolutePackPath lastFilePath = null; + + StringBuilder error = new StringBuilder(); + + for (AbsolutePackPath node : cycle) { + if (lastFilePath == null) { + lastFilePath = node; + continue; + } + + FileNode lastFile = nodes.get(lastFilePath); + int lineNumber = -1; + + for (Map.Entry include : lastFile.getIncludes().entrySet()) { + if (include.getValue() == node) { + lineNumber = include.getKey() + 1; + } + } + + String badLine = lastFile.getLines().get(lineNumber - 1); + + String detailMessage = node.equals(start) ? "final #include in cycle" : "#include involved in cycle"; + + if (lastFilePath.equals(start)) { + // first node in cycle + error.append(new RusticError("error", "#include cycle detected", + detailMessage, lastFilePath.getPathString(), lineNumber, badLine)); + } else { + error.append("\n = ").append(new RusticError("note", "cycle involves another file", + detailMessage, lastFilePath.getPathString(), lineNumber, badLine)); + } + + lastFilePath = node; + } + + error.append( + "\n = note: #include directives are resolved before any other preprocessor directives, any form of #include guard will not work" + + "\n = note: other cycles may still exist, only the first detected non-trivial cycle will be reported"); + + // TODO: Expose this to the caller (more semantic error handling) + Iris.logger.error(error.toString()); + + throw new IllegalStateException("Cycle detected in #include graph, see previous messages for details"); + } + } + } + + private boolean exploreForCycles(AbsolutePackPath frontier, List path, Set visited) { + if (visited.contains(frontier)) { + path.add(frontier); + return true; + } + + path.add(frontier); + visited.add(frontier); + + for (AbsolutePackPath included : nodes.get(frontier).getIncludes().values()) { + if (!nodes.containsKey(included)) { + // file that failed to load for another reason, error should already be reported + continue; + } + + if (exploreForCycles(included, path, visited)) { + return true; + } + } + + path.remove(path.size() - 1); + visited.remove(frontier); + + return false; + } + + public ImmutableMap getNodes() { + return nodes; + } + + public List computeWeaklyConnectedComponents() { + //List components = new ArrayList<>(); + + // TODO: WCC + //throw new UnsupportedOperationException(); + + //return components; + + // TODO: This digraph might not be weakly connected + // A digraph is weakly connected if its corresponding undirected + // graph is connected + // Make an adjacency list and then go from there + return Collections.singletonList(this); + } + + public IncludeGraph map(Function transformProvider) { + ImmutableMap.Builder mappedNodes = ImmutableMap.builder(); + + nodes.forEach((path, node) -> mappedNodes.put(path, node.map(transformProvider.apply(path)))); + + return new IncludeGraph(mappedNodes.build(), failures); + } + + public ImmutableMap getFailures() { + return failures; + } + + private static String readFile(Path path) throws IOException { + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/include/IncludeProcessor.java b/src/main/java/net/coderbot/iris/shaderpack/include/IncludeProcessor.java new file mode 100644 index 000000000..b06db48cb --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/include/IncludeProcessor.java @@ -0,0 +1,59 @@ +package net.coderbot.iris.shaderpack.include; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +// TODO: Write tests for this code +public class IncludeProcessor { + private final IncludeGraph graph; + private final Map> cache; + + public IncludeProcessor(IncludeGraph graph) { + this.graph = graph; + this.cache = new HashMap<>(); + } + + // TODO: Actual error handling + + public ImmutableList getIncludedFile(AbsolutePackPath path) { + ImmutableList lines = cache.get(path); + + if (lines == null) { + lines = process(path); + cache.put(path, lines); + } + + return lines; + } + + private ImmutableList process(AbsolutePackPath path) { + FileNode fileNode = graph.getNodes().get(path); + + if (fileNode == null) { + return null; + } + + ImmutableList.Builder builder = ImmutableList.builder(); + + ImmutableList lines = fileNode.getLines(); + ImmutableMap includes = fileNode.getIncludes(); + + for (int i = 0; i < lines.size(); i++) { + AbsolutePackPath include = includes.get(i); + + if (include != null) { + // TODO: Don't recurse like this, and check for cycles + // TODO: Better diagnostics + builder.addAll(Objects.requireNonNull(getIncludedFile(include))); + } else { + builder.add(lines.get(i)); + } + } + + return builder.build(); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/include/ShaderPackSourceNames.java b/src/main/java/net/coderbot/iris/shaderpack/include/ShaderPackSourceNames.java new file mode 100644 index 000000000..7709618e0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/include/ShaderPackSourceNames.java @@ -0,0 +1,97 @@ +package net.coderbot.iris.shaderpack.include; + +import com.google.common.collect.ImmutableList; +import net.coderbot.iris.shaderpack.loading.ProgramArrayId; +import net.coderbot.iris.shaderpack.loading.ProgramId; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Enumerates the possible program source file names to + */ +public class ShaderPackSourceNames { + public static final ImmutableList POTENTIAL_STARTS = findPotentialStarts(); + + public static boolean findPresentSources(ImmutableList.Builder starts, Path packRoot, + AbsolutePackPath directory, ImmutableList candidates) throws IOException { + Path directoryPath = directory.resolved(packRoot); + + if (!Files.exists(directoryPath)) { + return false; + } + + boolean anyFound = false; + + Set found; + try (Stream stream = Files.list(directoryPath)) { + found = stream + .map(path -> path.getFileName().toString()) + .collect(Collectors.toSet()); + } + + for (String candidate : candidates) { + if (found.contains(candidate)) { + starts.add(directory.resolve(candidate)); + anyFound = true; + } + } + + return anyFound; + } + + private static ImmutableList findPotentialStarts() { + ImmutableList.Builder potentialFileNames = ImmutableList.builder(); + + // TODO: Iterate over program groups for exact iteration order. + for (ProgramArrayId programArrayId : ProgramArrayId.values()) { + for (int i = 0; i < programArrayId.getNumPrograms(); i++) { + String name = programArrayId.getSourcePrefix(); + String suffix = ""; + + if (i > 0) { + suffix = Integer.toString(i); + } + + addComputeStarts(potentialFileNames, name + suffix); + } + } + + for (ProgramId programId : ProgramId.values()) { + if (programId == ProgramId.Final || programId == ProgramId.Shadow) { + addComputeStarts(potentialFileNames, programId.getSourceName()); + } else { + addStarts(potentialFileNames, programId.getSourceName()); + } + } + + return potentialFileNames.build(); + } + + private static void addStarts(ImmutableList.Builder potentialFileNames, String baseName) { + potentialFileNames.add(baseName + ".vsh"); + potentialFileNames.add(baseName + ".gsh"); + potentialFileNames.add(baseName + ".fsh"); + } + + private static void addComputeStarts(ImmutableList.Builder potentialFileNames, String baseName) { + addStarts(potentialFileNames, baseName); + + for (int j = 0; j < 27; j++) { + String suffix2; + + if (j == 0) { + suffix2 = ""; + } else { + char letter = (char) ('a' + j - 1); + suffix2 = "_" + letter; + } + + potentialFileNames.add(baseName + suffix2 + ".csh"); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramArrayId.java b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramArrayId.java new file mode 100644 index 000000000..1a1d0a941 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramArrayId.java @@ -0,0 +1,29 @@ +package net.coderbot.iris.shaderpack.loading; + +public enum ProgramArrayId { + ShadowComposite(ProgramGroup.ShadowComposite, 99), + Prepare(ProgramGroup.Prepare, 99), + Deferred(ProgramGroup.Deferred, 99), + Composite(ProgramGroup.Composite, 99), + ; + + private final ProgramGroup group; + private final int numPrograms; + + ProgramArrayId(ProgramGroup group, int numPrograms) { + this.group = group; + this.numPrograms = numPrograms; + } + + public ProgramGroup getGroup() { + return group; + } + + public String getSourcePrefix() { + return group.getBaseName(); + } + + public int getNumPrograms() { + return numPrograms; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramGroup.java b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramGroup.java new file mode 100644 index 000000000..468a7cd81 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramGroup.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.shaderpack.loading; + +public enum ProgramGroup { + Shadow("shadow"), + ShadowComposite("shadowcomp"), + Prepare("prepare"), + Gbuffers("gbuffers"), + Deferred("deferred"), + Composite("composite"), + Final("final") + ; + + private final String baseName; + + ProgramGroup(String baseName) { + this.baseName = baseName; + } + + public String getBaseName() { + return baseName; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramId.java b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramId.java new file mode 100644 index 000000000..1ae3ced09 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/loading/ProgramId.java @@ -0,0 +1,90 @@ +package net.coderbot.iris.shaderpack.loading; + +import net.coderbot.iris.gl.blending.BlendMode; +import net.coderbot.iris.gl.blending.BlendModeFunction; +import net.coderbot.iris.gl.blending.BlendModeOverride; + +import java.util.Objects; +import java.util.Optional; + +public enum ProgramId { + Shadow(ProgramGroup.Shadow, ""), + ShadowSolid(ProgramGroup.Shadow, "solid", Shadow), + ShadowCutout(ProgramGroup.Shadow, "cutout", Shadow), + + Basic(ProgramGroup.Gbuffers, "basic"), + Line(ProgramGroup.Gbuffers, "line", Basic), + + Textured(ProgramGroup.Gbuffers, "textured", Basic), + TexturedLit(ProgramGroup.Gbuffers, "textured_lit", Textured), + SkyBasic(ProgramGroup.Gbuffers, "skybasic", Basic), + SkyTextured(ProgramGroup.Gbuffers, "skytextured", Textured), + Clouds(ProgramGroup.Gbuffers, "clouds", Textured), + + Terrain(ProgramGroup.Gbuffers, "terrain", TexturedLit), + TerrainSolid(ProgramGroup.Gbuffers, "terrain_solid", Terrain), + TerrainCutoutMip(ProgramGroup.Gbuffers, "terrain_cutout_mip", Terrain), + TerrainCutout(ProgramGroup.Gbuffers, "terrain_cutout", Terrain), + DamagedBlock(ProgramGroup.Gbuffers, "damagedblock", Terrain), + + Block(ProgramGroup.Gbuffers, "block", Terrain), + BeaconBeam(ProgramGroup.Gbuffers, "beaconbeam", Textured), + Item(ProgramGroup.Gbuffers, "item", TexturedLit), + + Entities(ProgramGroup.Gbuffers, "entities", TexturedLit), + EntitiesTrans(ProgramGroup.Gbuffers, "entities_translucent", Entities), + EntitiesGlowing(ProgramGroup.Gbuffers, "entities_glowing", Entities), + ArmorGlint(ProgramGroup.Gbuffers, "armor_glint", Textured), + SpiderEyes(ProgramGroup.Gbuffers, "spidereyes", Textured, + new BlendModeOverride(new BlendMode(BlendModeFunction.SRC_ALPHA.getGlId(), BlendModeFunction.ONE.getGlId(), BlendModeFunction.ZERO.getGlId(), BlendModeFunction.ONE.getGlId()))), + + Hand(ProgramGroup.Gbuffers, "hand", TexturedLit), + Weather(ProgramGroup.Gbuffers, "weather", TexturedLit), + Water(ProgramGroup.Gbuffers, "water", Terrain), + HandWater(ProgramGroup.Gbuffers, "hand_water", Hand), + + Final(ProgramGroup.Final, ""), + ; + + private final ProgramGroup group; + private final String sourceName; + private final ProgramId fallback; + private final BlendModeOverride defaultBlendOverride; + + ProgramId(ProgramGroup group, String name) { + this.group = group; + this.sourceName = name.isEmpty() ? group.getBaseName() : group.getBaseName() + "_" + name; + this.fallback = null; + this.defaultBlendOverride = null; + } + + ProgramId(ProgramGroup group, String name, ProgramId fallback) { + this.group = group; + this.sourceName = name.isEmpty() ? group.getBaseName() : group.getBaseName() + "_" + name; + this.fallback = Objects.requireNonNull(fallback); + this.defaultBlendOverride = null; + } + + ProgramId(ProgramGroup group, String name, ProgramId fallback, BlendModeOverride defaultBlendOverride) { + this.group = group; + this.sourceName = name.isEmpty() ? group.getBaseName() : group.getBaseName() + "_" + name; + this.fallback = Objects.requireNonNull(fallback); + this.defaultBlendOverride = defaultBlendOverride; + } + + public ProgramGroup getGroup() { + return group; + } + + public String getSourceName() { + return sourceName; + } + + public Optional getFallback() { + return Optional.ofNullable(fallback); + } + + public BlendModeOverride getBlendModeOverride() { + return defaultBlendOverride; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/loading/SourceSet.java b/src/main/java/net/coderbot/iris/shaderpack/loading/SourceSet.java new file mode 100644 index 000000000..fb728561a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/loading/SourceSet.java @@ -0,0 +1,13 @@ +package net.coderbot.iris.shaderpack.loading; + +import java.util.function.Function; + +// TODO: Actually implement this class. +public class SourceSet { + //private final EnumMap singlePrograms; + //private final EnumMap programArrays; + + public SourceSet(Function sourceProvider) { + + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockEntry.java b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockEntry.java new file mode 100644 index 000000000..77388cea4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockEntry.java @@ -0,0 +1,110 @@ +package net.coderbot.iris.shaderpack.materialmap; + +import lombok.Getter; +import net.coderbot.iris.Iris; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@Getter +public class BlockEntry { + private final NamespacedId id; + private final Set metas; + + public BlockEntry(NamespacedId id, Set metas) { + this.id = id; + this.metas = metas; + } + + /** + * Parses a block ID entry. + * + * @param entry The string representation of the entry. Must not be empty. + */ + @NotNull + public static BlockEntry parse(@NotNull String entry) { + if (entry.isEmpty()) { + throw new IllegalArgumentException("Called BlockEntry::parse with an empty string"); + } + + // We can assume that this array is of at least array length because the input string is non-empty. + final String[] splitStates = entry.split(":"); + + // Trivial case: no states, no namespace + if (splitStates.length == 1) { + return new BlockEntry(new NamespacedId("minecraft", entry), Collections.emptySet()); + } + + // Examples of what we'll accept + // stone + // stone:0 + // minecraft:stone:0 + // minecraft:stone:0,1,2 # Theoretically valid, but I haven't seen any examples in actual shaders + + // Examples of what we don't (Yet?) accept - Seems to be from MC 1.8+ + // minecraft:farmland:moisture=0 + // minecraft:farmland:moisture=1 + // minecraft:double_plant:half=lower + // minecraft:double_plant:half=upper + // minecraft:grass:snowy=true + // minecraft:unpowered_comparator:powered=false + + + // Less trivial case: no metas involved, just a namespace + // + // The first term MUST be a valid ResourceLocation component + // The second term, if it is not numeric, must be a valid ResourceLocation component. + if (splitStates.length == 2 && !StringUtils.isNumeric(splitStates[1].substring(0, 1))) { + return new BlockEntry(new NamespacedId(splitStates[0], splitStates[1]), Collections.emptySet()); + } + + // Complex case: One or more states involved... + final int statesStart; + final NamespacedId id; + + if (StringUtils.isNumeric(splitStates[1].substring(0, 1))) { + // We have an entry of the form "stone:0" + statesStart = 1; + id = new NamespacedId("minecraft", splitStates[0]); + } else { + // We have an entry of the form "minecraft:stone:0" + statesStart = 2; + id = new NamespacedId(splitStates[0], splitStates[1]); + } + + final Set metas = new HashSet<>(); + + for (int index = statesStart; index < splitStates.length; index++) { + // Parse out one or more metadata ids + final String[] metaParts = splitStates[index].split(", "); + + for (String metaPart : metaParts) { + try { + metas.add(Integer.parseInt(metaPart)); + } catch (NumberFormatException e) { + Iris.logger.warn("Warning: the block ID map entry \"" + entry + "\" could not be fully parsed:"); + Iris.logger.warn("- Metadata ids must be a comma separated list of one or more integers, but "+ splitStates[index] + " is not of that form!"); + } + } + } + + return new BlockEntry(id, metas); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final BlockEntry that = (BlockEntry) o; + return Objects.equals(id, that.id) && Objects.equals(metas, that.metas); + } + + @Override + public int hashCode() { + return Objects.hash(id, metas); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockMatch.java b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockMatch.java new file mode 100644 index 000000000..06800592c --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockMatch.java @@ -0,0 +1,17 @@ +package net.coderbot.iris.shaderpack.materialmap; + +import net.minecraft.block.Block; + +public class BlockMatch { + private final Block block; + private final Integer meta; + + public BlockMatch(Block block, Integer meta) { + this.block = block; + this.meta = meta; + } + + public boolean matches(Block block, int meta) { + return this.block == block && (this.meta == null || this.meta == meta); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockRenderType.java b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockRenderType.java new file mode 100644 index 000000000..b2e2525ff --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/materialmap/BlockRenderType.java @@ -0,0 +1,46 @@ +package net.coderbot.iris.shaderpack.materialmap; + +import java.util.Optional; + +/** + * Defines the possible render types for blocks. + * + * @see + * the corresponding OptiFine documentation. + */ +public enum BlockRenderType { + /** + * Alpha test disabled, blending disabled, mipmaps enabled. + */ + SOLID, + /** + * Alpha test enabled, blending disabled, mipmaps disabled. + * Used for tall grass, normal glass, etc. + */ + CUTOUT, + /** + * Alpha test enabled, blending disabled, mipmaps enabled. + * Used for leaves. + */ + CUTOUT_MIPPED, + /** + * Alpha test enabled (w/ low cutoff), blending enabled, mipmaps enabled. + * Used for stained glass, nether portals, and water. + */ + TRANSLUCENT; + + public static Optional fromString(String name) { + switch (name) { + case "solid": + return Optional.of(SOLID); + case "cutout": + return Optional.of(CUTOUT); + case "cutout_mipped": + return Optional.of(CUTOUT_MIPPED); + case "translucent": + return Optional.of(TRANSLUCENT); + default: + return Optional.empty(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/materialmap/NamespacedId.java b/src/main/java/net/coderbot/iris/shaderpack/materialmap/NamespacedId.java new file mode 100644 index 000000000..30540d2af --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/materialmap/NamespacedId.java @@ -0,0 +1,61 @@ +package net.coderbot.iris.shaderpack.materialmap; + +import java.util.Objects; + +public class NamespacedId { + private final String namespace; + private final String name; + + public NamespacedId(String combined) { + int colonIdx = combined.indexOf(':'); + + if (colonIdx == -1) { + namespace = "minecraft"; + name = combined; + } else { + namespace = combined.substring(0, colonIdx); + name = combined.substring(colonIdx + 1); + } + } + + public NamespacedId(String namespace, String name) { + this.namespace = Objects.requireNonNull(namespace); + this.name = Objects.requireNonNull(name); + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + NamespacedId that = (NamespacedId) o; + + return namespace.equals(that.namespace) && name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(namespace, name); + } + + @Override + public String toString() { + return "NamespacedId{" + + "namespace='" + namespace + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/BaseOption.java b/src/main/java/net/coderbot/iris/shaderpack/option/BaseOption.java new file mode 100644 index 000000000..f1b3615ab --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/BaseOption.java @@ -0,0 +1,40 @@ +package net.coderbot.iris.shaderpack.option; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public abstract class BaseOption { + @NotNull + private final OptionType type; + @NotNull + private final String name; + @Nullable + private final String comment; + + BaseOption(@NotNull OptionType type, @NotNull String name, @Nullable String comment) { + this.type = type; + this.name = name; + + if (comment == null || comment.isEmpty()) { + this.comment = null; + } else { + this.comment = comment; + } + } + + @NotNull + public OptionType getType() { + return type; + } + + @NotNull + public String getName() { + return name; + } + + public Optional getComment() { + return Optional.ofNullable(comment); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/BooleanOption.java b/src/main/java/net/coderbot/iris/shaderpack/option/BooleanOption.java new file mode 100644 index 000000000..089837b83 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/BooleanOption.java @@ -0,0 +1,24 @@ +package net.coderbot.iris.shaderpack.option; + +public final class BooleanOption extends BaseOption { + private final boolean defaultValue; + + public BooleanOption(OptionType type, String name, String comment, boolean defaultValue) { + super(type, name, comment); + + this.defaultValue = defaultValue; + } + + public boolean getDefaultValue() { + return defaultValue; + } + + @Override + public String toString() { + return "BooleanDefineOption{" + + "name=" + getName() + + ", comment=" + getComment() + + ", defaultValue=" + defaultValue + + '}'; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/MergedBooleanOption.java b/src/main/java/net/coderbot/iris/shaderpack/option/MergedBooleanOption.java new file mode 100644 index 000000000..d3ea6963e --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/MergedBooleanOption.java @@ -0,0 +1,50 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Nullable; + +public class MergedBooleanOption { + private final BooleanOption option; + private final ImmutableSet locations; + + MergedBooleanOption(BooleanOption option, ImmutableSet locations) { + this.option = option; + this.locations = locations; + } + + public MergedBooleanOption(OptionLocation location, BooleanOption option) { + this.option = option; + this.locations = ImmutableSet.of(location); + } + + @Nullable + public MergedBooleanOption merge(MergedBooleanOption other) { + if (this.option.getDefaultValue() != other.option.getDefaultValue()) { + return null; + } + + BooleanOption option; + + // TODO: Collect all known comments + if (this.option.getComment().isPresent()) { + option = this.option; + } else { + option = other.option; + } + + ImmutableSet.Builder mergedLocations = ImmutableSet.builder(); + + mergedLocations.addAll(this.locations); + mergedLocations.addAll(other.locations); + + return new MergedBooleanOption(option, mergedLocations.build()); + } + + public BooleanOption getOption() { + return option; + } + + public ImmutableSet getLocations() { + return locations; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/MergedStringOption.java b/src/main/java/net/coderbot/iris/shaderpack/option/MergedStringOption.java new file mode 100644 index 000000000..9ab1afe6f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/MergedStringOption.java @@ -0,0 +1,50 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Nullable; + +public class MergedStringOption { + private final StringOption option; + private final ImmutableSet locations; + + MergedStringOption(StringOption option, ImmutableSet locations) { + this.option = option; + this.locations = locations; + } + + public MergedStringOption(OptionLocation location, StringOption option) { + this.option = option; + this.locations = ImmutableSet.of(location); + } + + @Nullable + public MergedStringOption merge(MergedStringOption other) { + if (!this.option.getDefaultValue().equals(other.option.getDefaultValue())) { + return null; + } + + StringOption option; + + // TODO: Collect all known comments + if (this.option.getComment().isPresent()) { + option = this.option; + } else { + option = other.option; + } + + ImmutableSet.Builder mergedLocations = ImmutableSet.builder(); + + mergedLocations.addAll(this.locations); + mergedLocations.addAll(other.locations); + + return new MergedStringOption(option, mergedLocations.build()); + } + + public StringOption getOption() { + return option; + } + + public ImmutableSet getLocations() { + return locations; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/OptionAnnotatedSource.java b/src/main/java/net/coderbot/iris/shaderpack/option/OptionAnnotatedSource.java new file mode 100644 index 000000000..8c0c9b478 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/OptionAnnotatedSource.java @@ -0,0 +1,563 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.include.AbsolutePackPath; +import net.coderbot.iris.shaderpack.option.values.OptionValues; +import net.coderbot.iris.shaderpack.parsing.ParsedString; +import net.coderbot.iris.shaderpack.transform.line.LineTransform; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class encapsulates the source code of a single shader source file along with the + * corresponding configurable options within the source file. + *

+ * The shader configuration system revolves around a carefully defined way of directly editing + * shader source files in order to change configuration options. This class handles the first + * step of that process—discovering configurable options from shader source files—as well as + * the final step of that process—editing shader source files to apply the modified values + * of valid configuration options. + *

+ * Intermediate steps of that process include considering the annotated source for all shader + * source files within a shader pack in order to deduplicate options that are common to multiple + * source files, and discarding options that are ambiguous between source files. In addition, + * another step includes loading changed option values from on-disk configuration files. + *

+ * The name "OptionAnnotatedSource" is based on the fact that this class simultaneously + * stores a snapshot of the shader source code at the time of option discovery, as well + * as data for each line ("annotations") about the relevant option represented by that + * line, or alternatively an optional diagnostic message for that line saying why a potential + * option was not parsed as a valid shader option. + *

+ * Note that for the most part, each line of the file is parsed in isolation from every + * other line. This means that option conflicts can arise even within the same source file, + * where option declarations have the same name and type but different default values. + * The only exception to this isolation is + * {@link OptionAnnotatedSource#getBooleanDefineReferences() boolean define reference tracking}, + * which is nevertheless still relatively context-free. + *

+ * The data stored within this class is immutable. This ensures that once you have discovered + * options from a given shader source file, that you may then apply any changed option values + * without having to re-parse the shader source code for options, and without risking having + * the shader source code fall out of sync with the annotations. + */ +public final class OptionAnnotatedSource { + /** + * The content of each line within this shader source file. + */ + private final ImmutableList lines; + + private final ImmutableMap booleanOptions; + private final ImmutableMap stringOptions; + + /** + * Optional diagnostic messages for each line. The parser may notice that though a shader pack + * author may have intended for a line to be a valid option, Iris might have ignored it due to + * a syntax error or some other issue. + * + * These diagnostic messages provide reasons for why Iris decided to ignore a plausible option + * line, as well as hints for how an invalid option line can be modified to be a valid one. + */ + private final ImmutableMap diagnostics; + + /** + * Keeps track of references to boolean #define options. Correlates the name of the #define + * option to one of the lines it was referenced on. + * + * References to boolean #define options that happen in plain #if directives are not analyzed + * for the purposes of determining whether a boolean #define option is referenced or not, to + * match OptiFine behavior. Though this might have originally been an oversight, shader packs + * now anticipate this behavior, so it must be replicated here. Since it would be complex to + * fully parse #if directives, this also makes the code simpler. + * + * Note that for the purposes of "confirming" a boolean #define option, it does not matter + * where the reference occurs in a given file - only that it is used at least once in the + * same "logical file" (that is, a file after all #includes have been processed) as it is + * defined. This is because shader config options are parsed as if all #include directives + * have already been substituted for the relevant file. + */ + // TODO: Use an immutable list type + private final ImmutableMap booleanDefineReferences; + + private static final ImmutableSet VALID_CONST_OPTION_NAMES = ImmutableSet.of( + "shadowMapResolution", + "shadowDistance", + "shadowDistanceRenderMul", + "entityShadowDistanceMul", + "shadowIntervalSize", + "generateShadowMipmap", + "generateShadowColorMipmap", + "shadowHardwareFiltering", + "shadowHardwareFiltering0", + "shadowHardwareFiltering1", + "shadowtex0Mipmap", + "shadowtexMipmap", + "shadowtex1Mipmap", + "shadowcolor0Mipmap", + "shadowColor0Mipmap", + "shadowcolor1Mipmap", + "shadowColor1Mipmap", + "shadowtex0Nearest", + "shadowtexNearest", + "shadow0MinMagNearest", + "shadowtex1Nearest", + "shadow1MinMagNearest", + "shadowcolor0Nearest", + "shadowColor0Nearest", + "shadowColor0MinMagNearest", + "shadowcolor1Nearest", + "shadowColor1Nearest", + "shadowColor1MinMagNearest", + "wetnessHalflife", + "drynessHalflife", + "eyeBrightnessHalflife", + "centerDepthHalflife", + "sunPathRotation", + "ambientOcclusionLevel", + "superSamplingLevel", + "noiseTextureResolution" + ); + + public OptionAnnotatedSource(final String source) { + // Match any valid newline sequence + // https://stackoverflow.com/a/31060125 + this(ImmutableList.copyOf(source.split("\\R"))); + } + + /** + * Parses the lines of a shader source file in order to locate valid options from it. + */ + public OptionAnnotatedSource(final ImmutableList lines) { + this.lines = lines; + + AnnotationsBuilder builder = new AnnotationsBuilder(); + + for (int index = 0; index < lines.size(); index++) { + String line = lines.get(index); + parseLine(builder, index, line); + } + + this.booleanOptions = builder.booleanOptions.build(); + this.stringOptions = builder.stringOptions.build(); + this.diagnostics = builder.diagnostics.build(); + this.booleanDefineReferences = ImmutableMap.copyOf(builder.booleanDefineReferences); + } + + private static void parseLine(AnnotationsBuilder builder, int index, String lineText) { + // Check to see if this line contains anything of interest before we try to parse it. + if (!lineText.contains("#define") + && !lineText.contains("const") + && !lineText.contains("#ifdef") + && !lineText.contains("#ifndef")) { + // Nothing of interest. + return; + } + + // Parse the trimmed form of the line to ignore indentation and trailing whitespace. + ParsedString line = new ParsedString(lineText.trim()); + + if (line.takeLiteral("#ifdef") || line.takeLiteral("#ifndef")) { + // The presence of #ifdef and #ifndef directives is used to determine whether a given + // boolean option should be recognized as a configurable option. + // + // As noted above, #if and #elif directives are not checked even though they may also + // contain references. + parseIfdef(builder, index, line); + } else if (line.takeLiteral("const")) { + parseConst(builder, index, line); + } else if (line.currentlyContains("#define")) { + parseDefineOption(builder, index, line); + } + } + + private static void parseIfdef(AnnotationsBuilder builder, int index, ParsedString line) { + if (!line.takeSomeWhitespace()) { + return; + } + + String name = line.takeWord(); + + line.takeSomeWhitespace(); + + if (name == null || !line.isEnd()) { + return; + } + + builder.booleanDefineReferences + .computeIfAbsent(name, n -> new IntArrayList()).add(index); + } + + private static void parseConst(AnnotationsBuilder builder, int index, ParsedString line) { + // const is already taken. + + if (!line.takeSomeWhitespace()) { + builder.diagnostics.put(index, "Expected whitespace after const and before type declaration"); + return; + } + + boolean isString; + + if (line.takeLiteral("int") || line.takeLiteral("float")) { + isString = true; + } else if (line.takeLiteral("bool")) { + isString = false; + } else { + builder.diagnostics.put(index, "Unexpected type declaration after const. " + + "Expected int, float, or bool. " + + "Vector const declarations cannot be configured using shader options."); + return; + } + + if (!line.takeSomeWhitespace()) { + builder.diagnostics.put(index, "Expected whitespace after type declaration."); + return; + } + + String name = line.takeWord(); + + if (name == null) { + builder.diagnostics.put(index, "Expected name of option after type declaration, " + + "but an unexpected character was detected first."); + return; + } + + line.takeSomeWhitespace(); + + if (!line.takeLiteral("=")) { + builder.diagnostics.put(index, "Unexpected characters before equals sign in const declaration."); + return; + } + + line.takeSomeWhitespace(); + + String value = line.takeWordOrNumber(); + + if (value == null) { + builder.diagnostics.put(index, "Unexpected non-whitespace characters after equals sign"); + return; + } + + line.takeSomeWhitespace(); + + if (!line.takeLiteral(";")) { + builder.diagnostics.put(index, "Value between the equals sign and the semicolon wasn't parsed as a valid word or number."); + return; + } + + line.takeSomeWhitespace(); + + String comment; + + if (line.takeComments()) { + comment = line.takeRest().trim(); + } else if (!line.isEnd()) { + builder.diagnostics.put(index, "Unexpected non-whitespace characters outside of comment after semicolon"); + return; + } else { + comment = null; + } + + if (!isString) { + boolean booleanValue; + + if ("true".equals(value)) { + booleanValue = true; + } else if ("false".equals(value)) { + booleanValue = false; + } else { + builder.diagnostics.put(index, "Expected true or false as the value of a boolean const option, but got " + + value + "."); + return; + } + + if (!VALID_CONST_OPTION_NAMES.contains(name)) { + builder.diagnostics.put(index, "This was a valid const boolean option declaration, but " + name + + " was not recognized as being a name of one of the configurable const options."); + return; + } + + builder.booleanOptions.put(index, new BooleanOption(OptionType.CONST, name, comment, booleanValue)); + return; + } + + if (!VALID_CONST_OPTION_NAMES.contains(name)) { + builder.diagnostics.put(index, "This was a valid const option declaration, but " + name + + " was not recognized as being a name of one of the configurable const options."); + return; + } + + StringOption option = StringOption.create(OptionType.CONST, name, comment, value); + + if (option != null) { + builder.stringOptions.put(index, option); + } else { + builder.diagnostics.put(index, "Ignoring this const option because it is missing an allowed values list" + + "in a comment, but is not a boolean const option."); + } + } + + private static void parseDefineOption(AnnotationsBuilder builder, int index, ParsedString line) { + // Remove the leading comment for processing. + boolean hasLeadingComment = line.takeComments(); + + // allow but do not require whitespace between comments and #define + line.takeSomeWhitespace(); + + if (!line.takeLiteral("#define")) { + builder.diagnostics.put(index, + "This line contains an occurrence of \"#define\" " + + "but it wasn't in a place we expected, ignoring it."); + return; + } + + if (!line.takeSomeWhitespace()) { + builder.diagnostics.put(index, + "This line properly starts with a #define statement but doesn't have " + + "any whitespace characters after the #define."); + return; + } + + String name = line.takeWord(); + + if (name == null) { + builder.diagnostics.put(index, + "Invalid syntax after #define directive. " + + "No alphanumeric or underscore characters detected."); + return; + } + + // Maybe take some whitespace + boolean tookWhitespace = line.takeSomeWhitespace(); + + if (line.isEnd()) { + // Plain define directive without a comment. + builder.booleanOptions.put(index, new BooleanOption(OptionType.DEFINE, name, null, !hasLeadingComment)); + return; + } + + if (line.takeComments()) { + // Note that this is a bare comment, we don't need to look for the allowed values part. + // Obviously that part isn't necessary since boolean options only have two possible + // values (true and false) + String comment = line.takeRest().trim(); + + builder.booleanOptions.put(index, new BooleanOption(OptionType.DEFINE, name, comment, !hasLeadingComment)); + return; + } else if (!tookWhitespace) { + // Invalid syntax. + builder.diagnostics.put(index, + "Invalid syntax after #define directive. Only alphanumeric or underscore " + + "characters are allowed in option names."); + + return; + } + + if (hasLeadingComment) { + builder.diagnostics.put(index, + "Ignoring potential non-boolean #define option since it has a leading comment. " + + "Leading comments (//) are only allowed on boolean #define options."); + return; + } + + String value = line.takeWordOrNumber(); + + if (value == null) { + builder.diagnostics.put(index, "Ignoring this #define directive because it doesn't appear to be a boolean #define, " + + "and its potential value wasn't a valid number or a valid word."); + return; + } + + tookWhitespace = line.takeSomeWhitespace(); + + if (line.isEnd()) { + builder.diagnostics.put(index, "Ignoring this #define because it doesn't have a comment containing" + + " a list of allowed values afterwards, but it has a value so is therefore not a boolean."); + return; + } else if (!tookWhitespace) { + if (!line.takeComments()) { + builder.diagnostics.put(index, + "Invalid syntax after value #define directive. " + + "Invalid characters after number or word."); + return; + } + } else if (!line.takeComments()) { + builder.diagnostics.put(index, + "Invalid syntax after value #define directive. " + + "Only comments may come after the value."); + return; + } + + String comment = line.takeRest().trim(); + + StringOption option = StringOption.create(OptionType.DEFINE, name, comment, value); + + if (option == null) { + builder.diagnostics.put(index, "Ignoring this #define because it is missing an allowed values list" + + "in a comment, but is not a boolean define."); + return; + } + + builder.stringOptions.put(index, option); + + /* + //#define SHADOWS // Whether shadows are enabled + SHADOWS // Whether shadows are enabled + // Whether shadows are enabled + Whether shadows are enabled + + + + #define OPTION 0.5 // A test option + OPTION 0.5 // A test option + 0.5 // A test option + */ + } + + public ImmutableMap getBooleanOptions() { + return booleanOptions; + } + + public ImmutableMap getStringOptions() { + return stringOptions; + } + + public ImmutableMap getDiagnostics() { + return diagnostics; + } + + public ImmutableMap getBooleanDefineReferences() { + return booleanDefineReferences; + } + + public OptionSet getOptionSet(AbsolutePackPath filePath, Set booleanDefineReferences) { + OptionSet.Builder builder = OptionSet.builder(); + + booleanOptions.forEach((lineIndex, option) -> { + if (booleanDefineReferences.contains(option.getName())) { + OptionLocation location = new OptionLocation(filePath, lineIndex); + builder.addBooleanOption(location, option); + } + }); + + stringOptions.forEach((lineIndex, option) -> { + OptionLocation location = new OptionLocation(filePath, lineIndex); + builder.addStringOption(location, option); + }); + + return builder.build(); + } + + public LineTransform asTransform(OptionValues values) { + return (index, line) -> edit(values, index, line); + } + + public String apply(OptionValues values) { + StringBuilder source = new StringBuilder(); + + for (int index = 0; index < lines.size(); index++) { + source.append(edit(values, index, lines.get(index))); + source.append('\n'); + } + + return source.toString(); + } + + private String edit(OptionValues values, int index, String existing) { + // See if it's a boolean option + BooleanOption booleanOption = booleanOptions.get(index); + + if (booleanOption != null) { + OptionalBoolean value = values.getBooleanValue(booleanOption.getName()); + if (booleanOption.getType() == OptionType.DEFINE) { + return setBooleanDefineValue(existing, value, booleanOption.getDefaultValue()); + } else if (booleanOption.getType() == OptionType.CONST) { + if (value != OptionalBoolean.DEFAULT) { + // Value will never be default here, but we're using orElse just to get a normal boolean out of it. + return editConst(existing, Boolean.toString(booleanOption.getDefaultValue()), Boolean.toString(value.orElse(booleanOption.getDefaultValue()))); + } else { + return existing; + } + } else { + throw new AssertionError("Unknown option type " + booleanOption.getType()); + } + } + + StringOption stringOption = stringOptions.get(index); + + if (stringOption != null) { + return values.getStringValue(stringOption.getName()).map(value -> { + if (stringOption.getType() == OptionType.DEFINE) { + return "#define " + stringOption.getName() + " " + value + " // OptionAnnotatedSource: Changed option"; + } else if (stringOption.getType() == OptionType.CONST) { + return editConst(existing, stringOption.getDefaultValue(), value); + } else { + throw new AssertionError("Unknown option type " + stringOption.getType()); + } + }).orElse(existing); + } + + return existing; + } + + private String editConst(String line, String currentValue, String newValue) { + int equalsIndex = line.indexOf('='); + + if (equalsIndex == -1) { + // This shouldn't be possible. + throw new IllegalStateException(); + } + + String firstPart = line.substring(0, equalsIndex); + String secondPart = line.substring(equalsIndex); + + secondPart = secondPart.replaceFirst(Pattern.quote(currentValue), Matcher.quoteReplacement(newValue)); + + return firstPart + secondPart; + } + + private static boolean hasLeadingComment(String line) { + return line.trim().startsWith("//"); + } + + private static String removeLeadingComment(String line) { + ParsedString parsed = new ParsedString(line); + + parsed.takeSomeWhitespace(); + parsed.takeComments(); + + return parsed.takeRest(); + } + + private static String setBooleanDefineValue(String line, OptionalBoolean newValue, boolean defaultValue) { + if (hasLeadingComment(line) && newValue.orElse(defaultValue)) { + return removeLeadingComment(line); + } else if (!newValue.orElse(defaultValue)) { + return "//" + line; + } else { + return line; + } + } + + private static class AnnotationsBuilder { + private final ImmutableMap.Builder booleanOptions; + private final ImmutableMap.Builder stringOptions; + private final ImmutableMap.Builder diagnostics; + private final Map booleanDefineReferences; + + private AnnotationsBuilder() { + booleanOptions = ImmutableMap.builder(); + stringOptions = ImmutableMap.builder(); + diagnostics = ImmutableMap.builder(); + booleanDefineReferences = new HashMap<>(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/OptionLocation.java b/src/main/java/net/coderbot/iris/shaderpack/option/OptionLocation.java new file mode 100644 index 000000000..36a078dd5 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/OptionLocation.java @@ -0,0 +1,29 @@ +package net.coderbot.iris.shaderpack.option; + +import net.coderbot.iris.shaderpack.include.AbsolutePackPath; + +/** + * Encapsulates a single location of an option. + */ +public class OptionLocation { + private final AbsolutePackPath filePath; + private final int lineIndex; + + public OptionLocation(AbsolutePackPath filePath, int lineIndex) { + this.filePath = filePath; + this.lineIndex = lineIndex; + } + + public AbsolutePackPath getFilePath() { + return filePath; + } + + /** + * Gets the index of the line this option is on. + * Note that this is the index - so the first line is + * 0, the second is 1, etc. + */ + public int getLineIndex() { + return lineIndex; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/OptionSet.java b/src/main/java/net/coderbot/iris/shaderpack/option/OptionSet.java new file mode 100644 index 000000000..27db1825c --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/OptionSet.java @@ -0,0 +1,113 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.Iris; + +import java.util.HashMap; +import java.util.Map; + +public class OptionSet { + private final ImmutableMap booleanOptions; + private final ImmutableMap stringOptions; + + private OptionSet(Builder builder) { + this.booleanOptions = ImmutableMap.copyOf(builder.booleanOptions); + this.stringOptions = ImmutableMap.copyOf(builder.stringOptions); + } + + public ImmutableMap getBooleanOptions() { + return this.booleanOptions; + } + + public ImmutableMap getStringOptions() { + return this.stringOptions; + } + + public boolean isBooleanOption(String name) { + return booleanOptions.containsKey(name); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map booleanOptions; + private final Map stringOptions; + + public Builder() { + this.booleanOptions = new HashMap<>(); + this.stringOptions = new HashMap<>(); + } + + public void addAll(OptionSet other) { + if (this.booleanOptions.isEmpty()) { + this.booleanOptions.putAll(other.booleanOptions); + } else { + other.booleanOptions.values().forEach(this::addBooleanOption); + } + + if (this.stringOptions.isEmpty()) { + this.stringOptions.putAll(other.stringOptions); + } else { + other.stringOptions.values().forEach(this::addStringOption); + } + } + + public void addBooleanOption(OptionLocation location, BooleanOption option) { + addBooleanOption(new MergedBooleanOption(location, option)); + } + + public void addBooleanOption(MergedBooleanOption proposed) { + BooleanOption option = proposed.getOption(); + MergedBooleanOption existing = booleanOptions.get(option.getName()); + + MergedBooleanOption merged; + + if (existing != null) { + merged = existing.merge(proposed); + + if (merged == null) { + // TODO: Warn about ambiguous options better + Iris.logger.warn("Ignoring ambiguous boolean option " + option.getName()); + booleanOptions.remove(option.getName()); + return; + } + } else { + merged = proposed; + } + + booleanOptions.put(option.getName(), merged); + } + + public void addStringOption(OptionLocation location, StringOption option) { + addStringOption(new MergedStringOption(location, option)); + } + + public void addStringOption(MergedStringOption proposed) { + StringOption option = proposed.getOption(); + MergedStringOption existing = stringOptions.get(option.getName()); + + MergedStringOption merged; + + if (existing != null) { + merged = existing.merge(proposed); + + if (merged == null) { + // TODO: Warn about ambiguous options better + Iris.logger.warn("Ignoring ambiguous string option " + option.getName()); + stringOptions.remove(option.getName()); + return; + } + } else { + merged = proposed; + } + + stringOptions.put(option.getName(), merged); + } + + public OptionSet build() { + return new OptionSet(this); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/OptionTests.java b/src/main/java/net/coderbot/iris/shaderpack/option/OptionTests.java new file mode 100644 index 000000000..395969cd4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/OptionTests.java @@ -0,0 +1,68 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class OptionTests { + public static void main(String[] args) throws IOException { + List lines = Files.readAllLines(Paths.get("run/shaderpacks/Sildurs Vibrant Shaders v1.29 Medium/shaders/shaders.settings")); + + OptionAnnotatedSource source = new OptionAnnotatedSource(ImmutableList.copyOf(lines)); + + System.out.println("Boolean Options:"); + + if (source.getBooleanOptions().isEmpty()) { + System.out.println("(none)"); + } else { + System.out.println("[Line] Type | Name | Value | Comment"); + System.out.println(" ------ | -------------------------------- | ----- | -------"); + } + + source.getBooleanOptions().forEach((index, option) -> { + String type = option.getType() == OptionType.CONST ? " Const" : "Define"; + + System.out.println( + "[" + StringUtils.leftPad(Integer.toString(index + 1), 4, ' ') + "] " + type + " | " + + StringUtils.rightPad(option.getName(), 32, ' ') + " | " + + StringUtils.leftPad(Boolean.toString(option.getDefaultValue()), 5, ' ') + + " | " + option.getComment().orElse("")); + }); + + + System.out.println("String Options:"); + + if (source.getStringOptions().isEmpty()) { + System.out.println("(none)"); + } else { + System.out.println("[Line] | Type | Name | Value | Allowed Values"); + System.out.println(" | ------ | -------------------------------- | -------- | -------"); + } + + source.getStringOptions().forEach((index, option) -> { + String type = option.getType() == OptionType.CONST ? " Const" : "Define"; + + System.out.println( + "[" + StringUtils.leftPad(Integer.toString(index + 1), 4, ' ') + "] | " + type + " | " + + StringUtils.rightPad(option.getName(), 32, ' ') + " | " + + StringUtils.leftPad(option.getDefaultValue(), 8, ' ') + + " | " + option.getAllowedValues()); + System.out.println(" " + option.getComment().orElse("(no comment)")); + }); + + System.out.println("Diagnostics:"); + source.getDiagnostics().forEach((index, diagnostic) -> { + System.out.println( + "[" + StringUtils.leftPad(Integer.toString(index + 1), 4, ' ') + "] " + + diagnostic); + }); + + if (source.getDiagnostics().isEmpty()) { + System.out.println("(none)"); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/OptionType.java b/src/main/java/net/coderbot/iris/shaderpack/option/OptionType.java new file mode 100644 index 000000000..8ff93bf9a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/OptionType.java @@ -0,0 +1,5 @@ +package net.coderbot.iris.shaderpack.option; + +public enum OptionType { + DEFINE, CONST +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/Profile.java b/src/main/java/net/coderbot/iris/shaderpack/option/Profile.java new file mode 100644 index 000000000..4f6620f48 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/Profile.java @@ -0,0 +1,81 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class Profile { + public final String name; + public final int precedence; // Used for prioritizing during matching + public final Map optionValues; + public final List disabledPrograms; + + private Profile(String name, Map optionValues, List disabledPrograms) { + this.name = name; + this.optionValues = optionValues; + this.precedence = optionValues.size(); + this.disabledPrograms = disabledPrograms; + } + + public boolean matches(OptionSet options, OptionValues values) { + for (Map.Entry entry : this.optionValues.entrySet()) { + String option = entry.getKey(); + String value = entry.getValue(); + + if (options.getBooleanOptions().containsKey(option)) { + boolean currentValue = values.getBooleanValueOrDefault(option); + + if (!Boolean.toString(currentValue).equals(value)) { + return false; + } + } + if (options.getStringOptions().containsKey(option)) { + String currentValue = values.getStringValueOrDefault(option); + + if (!value.equals(currentValue)) { + return false; + } + } + } + + return true; + } + + public static class Builder { + private final String name; + private final Map optionValues = new HashMap<>(); + private final List disabledPrograms = new ArrayList<>(); + + public Builder(String name) { + this.name = name; + } + + public Builder option(String optionId, String value) { + this.optionValues.put(optionId, value); + + return this; + } + + public Builder disableProgram(String programId) { + this.disabledPrograms.add(programId); + + return this; + } + + public Builder addAll(Profile other) { + this.optionValues.putAll(other.optionValues); + this.disabledPrograms.addAll(other.disabledPrograms); + + return this; + } + + public Profile build() { + return new Profile(name, ImmutableMap.copyOf(optionValues), ImmutableList.copyOf(disabledPrograms)); + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/ProfileSet.java b/src/main/java/net/coderbot/iris/shaderpack/option/ProfileSet.java new file mode 100644 index 000000000..fd6f086b7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/ProfileSet.java @@ -0,0 +1,130 @@ +package net.coderbot.iris.shaderpack.option; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.IrisLogging; +import net.coderbot.iris.shaderpack.option.values.OptionValues; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +public class ProfileSet { + private final LinkedHashMap orderedProfiles; // The order that profiles should cycle through + private final List sortedProfiles; // The order that profiles should be scanned through + + public ProfileSet(LinkedHashMap orderedProfiles) { + List sorted = new ArrayList<>(orderedProfiles.values()); + + Comparator lowToHigh = Comparator.comparing(p -> p.precedence); + Comparator highToLow = lowToHigh.reversed(); + + // Compare profiles with many constraints (high precedence) first before comparing ones with + // few constraints, needed for accurate matching when one profile contains an additional constraint + // to another profile but is otherwise the same; + sorted.sort(highToLow); + + if (IrisLogging.ENABLE_SPAM) { + sorted.forEach(p -> System.out.println(p.name + ":" + p.precedence)); + } + + this.sortedProfiles = sorted; + this.orderedProfiles = orderedProfiles; + } + + public void forEach(BiConsumer action) { + orderedProfiles.forEach(action); + } + + public ProfileResult scan(OptionSet options, OptionValues values) { + if (sortedProfiles.size() <= 0) { + return new ProfileResult(null, null, null); + } + + for (int i = 0; i < sortedProfiles.size(); i++) { + Profile current = sortedProfiles.get(i); + + if (current.matches(options, values)) { + Profile next = sortedProfiles.get(Math.floorMod(i + 1, sortedProfiles.size())); + Profile prev = sortedProfiles.get(Math.floorMod(i - 1, sortedProfiles.size())); + + return new ProfileResult(current, next, prev); + } + } + + // Default return if no profiles matched + Profile next = sortedProfiles.get(0); + Profile prev = sortedProfiles.get(sortedProfiles.size() - 1); + + return new ProfileResult(null, next, prev); + } + + public static ProfileSet fromTree(Map> tree, OptionSet optionSet) { + LinkedHashMap profiles = new LinkedHashMap<>(); + + for (String name : tree.keySet()) { + profiles.put(name, parse(name, new ArrayList<>(), tree, optionSet)); + } + + return new ProfileSet(profiles); + } + + private static Profile parse(String name, List parents, Map> tree, OptionSet optionSet) throws IllegalArgumentException { + Profile.Builder builder = new Profile.Builder(name); + List options = tree.get(name); + + if (options == null) { + throw new IllegalArgumentException("Profile \"" + name + "\" does not exist!"); + } + + for (String option : options) { + if (option.startsWith("!program.")) { + builder.disableProgram(option.substring("!program.".length())); + } else if (option.startsWith("profile.")) { + String dependency = option.substring("profile.".length()); + + if (parents.contains(dependency)) { + throw new IllegalArgumentException("Error parsing profile \"" + name + + "\", recursively included by: " + String.join(", ", parents)); + } + + parents.add(dependency); + builder.addAll(parse(dependency, parents, tree, optionSet)); + } else if (option.startsWith("!")) { + builder.option(option.substring(1), "false"); + } else if (option.contains("=")) { + int splitPoint = option.indexOf("="); + builder.option(option.substring(0, splitPoint), option.substring(splitPoint + 1)); + } else if (option.contains(":")) { + int splitPoint = option.indexOf(":"); + builder.option(option.substring(0, splitPoint), option.substring(splitPoint + 1)); + } else if (optionSet.isBooleanOption(option)) { + builder.option(option, "true"); + } else { + Iris.logger.warn("Invalid pack option: " + option); + } + } + + return builder.build(); + } + + public int size() { + return sortedProfiles.size(); + } + + public static class ProfileResult { + public final Optional current; + public final Profile next; + public final Profile previous; + + private ProfileResult(@Nullable Profile current, Profile next, Profile previous) { + this.current = Optional.ofNullable(current); + this.next = next; + this.previous = previous; + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/ShaderPackOptions.java b/src/main/java/net/coderbot/iris/shaderpack/option/ShaderPackOptions.java new file mode 100644 index 000000000..d3aabb954 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/ShaderPackOptions.java @@ -0,0 +1,66 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.shaderpack.include.AbsolutePackPath; +import net.coderbot.iris.shaderpack.include.IncludeGraph; +import net.coderbot.iris.shaderpack.option.values.MutableOptionValues; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A helper class that dispatches all the heavy lifting needed to discover, merge, and apply shader pack options to + * an existing {@link IncludeGraph}. + */ +public class ShaderPackOptions { + private final OptionSet optionSet; + private final OptionValues optionValues; + private final IncludeGraph includes; + + public ShaderPackOptions(IncludeGraph graph, Map changedConfigs) { + Map allAnnotations = new HashMap<>(); + OptionSet.Builder setBuilder = OptionSet.builder(); + + graph.computeWeaklyConnectedComponents().forEach(subgraph -> { + ImmutableMap.Builder annotationBuilder = ImmutableMap.builder(); + Set referencedBooleanDefines = new HashSet<>(); + + subgraph.getNodes().forEach((path, node) -> { + OptionAnnotatedSource annotatedSource = new OptionAnnotatedSource(node.getLines()); + annotationBuilder.put(path, annotatedSource); + referencedBooleanDefines.addAll(annotatedSource.getBooleanDefineReferences().keySet()); + }); + + ImmutableMap annotations = annotationBuilder.build(); + Set referencedBooleanDefinesU = Collections.unmodifiableSet(referencedBooleanDefines); + + annotations.forEach((path, annotatedSource) -> { + OptionSet set = annotatedSource.getOptionSet(path, referencedBooleanDefinesU); + setBuilder.addAll(set); + }); + + allAnnotations.putAll(annotations); + }); + + this.optionSet = setBuilder.build(); + this.optionValues = new MutableOptionValues(optionSet, changedConfigs); + + this.includes = graph.map(path -> allAnnotations.get(path).asTransform(optionValues)); + } + + public OptionSet getOptionSet() { + return optionSet; + } + + public OptionValues getOptionValues() { + return optionValues; + } + + public IncludeGraph getIncludes() { + return includes; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/StringOption.java b/src/main/java/net/coderbot/iris/shaderpack/option/StringOption.java new file mode 100644 index 000000000..791b6a534 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/StringOption.java @@ -0,0 +1,76 @@ +package net.coderbot.iris.shaderpack.option; + +import com.google.common.collect.ImmutableList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class StringOption extends BaseOption { + private final String defaultValue; + private final ImmutableList allowedValues; + + private StringOption(OptionType type, String name, String defaultValue) { + super(type, name, null); + + this.defaultValue = Objects.requireNonNull(defaultValue); + this.allowedValues = ImmutableList.of(defaultValue); + } + + private StringOption(OptionType type, String name, String comment, String defaultValue, ImmutableList allowedValues) { + super(type, name, comment); + + this.defaultValue = Objects.requireNonNull(defaultValue); + this.allowedValues = allowedValues; + } + + @Nullable + public static StringOption create(OptionType type, String name, String comment, String defaultValue) { + if (comment == null) { + return null; + } + + int openingBracket = comment.indexOf('['); + + if (openingBracket == -1) { + return null; + } + + int closingBracket = comment.indexOf(']', openingBracket); + + if (closingBracket == -1) { + return null; + } + + String[] allowedValues = comment.substring(openingBracket + 1, closingBracket).split(" "); + comment = comment.substring(0, openingBracket) + comment.substring(closingBracket + 1); + boolean allowedValuesContainsDefaultValue = false; + + for (String value : allowedValues) { + if (defaultValue.equals(value)) { + allowedValuesContainsDefaultValue = true; + break; + } + } + + ImmutableList.Builder builder = ImmutableList.builder(); + + builder.add(allowedValues); + + if (!allowedValuesContainsDefaultValue) { + builder.add(defaultValue); + } + + return new StringOption(type, name, comment.trim(), defaultValue, builder.build()); + } + + @NotNull + public String getDefaultValue() { + return defaultValue; + } + + @NotNull + public ImmutableList getAllowedValues() { + return allowedValues; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuBooleanOptionElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuBooleanOptionElement.java new file mode 100644 index 000000000..d40ee3539 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuBooleanOptionElement.java @@ -0,0 +1,14 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.BooleanOption; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +public class OptionMenuBooleanOptionElement extends OptionMenuOptionElement { + public final BooleanOption option; + + public OptionMenuBooleanOptionElement(String elementString, OptionMenuContainer container, ShaderProperties shaderProperties, OptionValues values, BooleanOption option) { + super(elementString, container, shaderProperties, values); + this.option = option; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java new file mode 100644 index 000000000..fa4712978 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java @@ -0,0 +1,89 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import com.google.common.collect.Lists; +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.ProfileSet; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class OptionMenuContainer { + public final OptionMenuElementScreen mainScreen; + public final Map subScreens = new HashMap<>(); + + private final List usedOptionElements = new ArrayList<>(); + private final List usedOptions = new ArrayList<>(); + private final List unusedOptions = new ArrayList<>(); // To be used when screens contain a "*" element + private final Map, Integer> unusedOptionDumpQueue = new HashMap<>(); // Used by screens with "*" element + private final ProfileSet profiles; + + public OptionMenuContainer(ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, ProfileSet profiles) { + this.profiles = profiles; + + // note: if the Shader Pack does not provide a list of options for the main screen, then dump all options on to + // the main screen by default. + this.mainScreen = new OptionMenuMainElementScreen( + this, shaderProperties, shaderPackOptions, + shaderProperties.getMainScreenOptions().orElseGet(() -> Collections.singletonList("*")), + shaderProperties.getMainScreenColumnCount()); + + this.unusedOptions.addAll(shaderPackOptions.getOptionSet().getBooleanOptions().keySet()); + this.unusedOptions.addAll(shaderPackOptions.getOptionSet().getStringOptions().keySet()); + + Map subScreenColumnCounts = shaderProperties.getSubScreenColumnCount(); + shaderProperties.getSubScreenOptions().forEach((screenKey, options) -> { + subScreens.put(screenKey, new OptionMenuSubElementScreen( + screenKey, this, shaderProperties, shaderPackOptions, options, Optional.ofNullable(subScreenColumnCounts.get(screenKey)))); + }); + + // Dump all unused options into screens containing "*" + for (Map.Entry, Integer> entry : unusedOptionDumpQueue.entrySet()) { + List elementsToInsert = new ArrayList<>(); + List unusedOptionsCopy = Lists.newArrayList(this.unusedOptions); + + for (String optionId : unusedOptionsCopy) { + try { + OptionMenuElement element = OptionMenuElement.create(optionId, this, shaderProperties, shaderPackOptions); + if (element != null) { + elementsToInsert.add(element); + + if (element instanceof OptionMenuOptionElement) { + this.notifyOptionAdded(optionId, (OptionMenuOptionElement) element); + } + } + } catch (IllegalArgumentException error) { + Iris.logger.warn(error); + + elementsToInsert.add(OptionMenuElement.EMPTY); + } + } + + entry.getKey().addAll(entry.getValue(), elementsToInsert); + } + } + + public ProfileSet getProfiles() { + return profiles; + } + + // Screens will call this when they contain a "*" element, so that the list of + // unused options can be added after all other screens have been resolved + public void queueForUnusedOptionDump(int index, List elementList) { + this.unusedOptionDumpQueue.put(elementList, index); + } + + public void notifyOptionAdded(String optionId, OptionMenuOptionElement option) { + if (!usedOptions.contains(optionId)) { + usedOptionElements.add(option); + usedOptions.add(optionId); + } + + unusedOptions.remove(optionId); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElement.java new file mode 100644 index 000000000..1e784534f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElement.java @@ -0,0 +1,45 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.MergedBooleanOption; +import net.coderbot.iris.shaderpack.option.MergedStringOption; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; + +import java.util.Map; + +public abstract class OptionMenuElement { + public static final OptionMenuElement EMPTY = new OptionMenuElement() {}; + + private static final String ELEMENT_EMPTY = ""; + private static final String ELEMENT_PROFILE = ""; + + public static OptionMenuElement create(String elementString, OptionMenuContainer container, ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions) throws IllegalArgumentException { + // Empty element + if (ELEMENT_EMPTY.equals(elementString)) { + return EMPTY; + } + // Profile element + // NB: We don't want to add a profile element if there aren't profiles, even if it's requested. TODO: add this to diagnostics? (lp) + if (ELEMENT_PROFILE.equals(elementString)) { + // null indicates that the element should be forgotten (not treated as empty) + return container.getProfiles().size() > 0 ? new OptionMenuProfileElement(container.getProfiles(), shaderPackOptions.getOptionSet(), shaderPackOptions.getOptionValues()) : null; + } + // Link to sub screen element + if (elementString.startsWith("[") && elementString.endsWith("]")) { + return new OptionMenuLinkElement(elementString.substring(1, elementString.length() - 1)); + } + + Map booleanOptions = shaderPackOptions.getOptionSet().getBooleanOptions(); + Map stringOptions = shaderPackOptions.getOptionSet().getStringOptions(); + + // Option elements (boolean and string), only succeed if the option is defined in the shader source + if (booleanOptions.containsKey(elementString)) { + return new OptionMenuBooleanOptionElement(elementString, container, shaderProperties, shaderPackOptions.getOptionValues(), booleanOptions.get(elementString).getOption()); + } else if (stringOptions.containsKey(elementString)) { + return new OptionMenuStringOptionElement(elementString, container, shaderProperties, shaderPackOptions.getOptionValues(), stringOptions.get(elementString).getOption()); + } + + // Handled and ignored with log warning + throw new IllegalArgumentException("Unable to resolve shader pack option menu element \"" + elementString + "\" defined in shaders.properties"); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElementScreen.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElementScreen.java new file mode 100644 index 000000000..38185a323 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuElementScreen.java @@ -0,0 +1,47 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class OptionMenuElementScreen { + public final List elements = new ArrayList<>(); + + private final Optional columnCount; + + public OptionMenuElementScreen(OptionMenuContainer container, ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, List elementStrings, Optional columnCount) { + this.columnCount = columnCount; + + for (String elementString : elementStrings) { + if ("*".equals(elementString)) { + container.queueForUnusedOptionDump(this.elements.size(), this.elements); + + continue; + } + + try { + OptionMenuElement element = OptionMenuElement.create(elementString, container, shaderProperties, shaderPackOptions); + + if (element != null) { + this.elements.add(element); + + if (element instanceof OptionMenuOptionElement) { + container.notifyOptionAdded(elementString, (OptionMenuOptionElement) element); + } + } + } catch (IllegalArgumentException error) { + Iris.logger.warn(error.getMessage()); + + this.elements.add(OptionMenuElement.EMPTY); + } + } + } + + public int getColumnCount() { + return columnCount.orElse(elements.size() > 18 ? 3 : 2); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuLinkElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuLinkElement.java new file mode 100644 index 000000000..cc82cb72b --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuLinkElement.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.shaderpack.option.menu; + +public class OptionMenuLinkElement extends OptionMenuElement { + public final String targetScreenId; + + public OptionMenuLinkElement(String targetScreenId) { + this.targetScreenId = targetScreenId; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuMainElementScreen.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuMainElementScreen.java new file mode 100644 index 000000000..fbd9476f4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuMainElementScreen.java @@ -0,0 +1,13 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; + +import java.util.List; +import java.util.Optional; + +public class OptionMenuMainElementScreen extends OptionMenuElementScreen { + public OptionMenuMainElementScreen(OptionMenuContainer container, ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, List elementStrings, Optional columnCount) { + super(container, shaderProperties, shaderPackOptions, elementStrings, columnCount); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuOptionElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuOptionElement.java new file mode 100644 index 000000000..c25646f88 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuOptionElement.java @@ -0,0 +1,39 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.values.MutableOptionValues; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +public abstract class OptionMenuOptionElement extends OptionMenuElement { + public final boolean slider; + public final OptionMenuContainer container; + public final String optionId; + + private final OptionValues packAppliedValues; + + public OptionMenuOptionElement(String elementString, OptionMenuContainer container, ShaderProperties shaderProperties, OptionValues packAppliedValues) { + this.slider = shaderProperties.getSliderOptions().contains(elementString); + this.container = container; + this.optionId = elementString; + this.packAppliedValues = packAppliedValues; + } + + /** + * @return the {@link OptionValues} currently in use by the shader pack + */ + public OptionValues getAppliedOptionValues() { + return packAppliedValues; + } + + /** + * @return an {@link OptionValues} that also contains values currently + * pending application. + */ + public OptionValues getPendingOptionValues() { + MutableOptionValues values = getAppliedOptionValues().mutableCopy(); + values.addAll(Iris.getShaderPackOptionQueue()); + + return values; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuProfileElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuProfileElement.java new file mode 100644 index 000000000..78ba4fa13 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuProfileElement.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.option.OptionSet; +import net.coderbot.iris.shaderpack.option.ProfileSet; +import net.coderbot.iris.shaderpack.option.values.MutableOptionValues; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +public class OptionMenuProfileElement extends OptionMenuElement { + public final ProfileSet profiles; + public final OptionSet options; + + private final OptionValues packAppliedValues; + + public OptionMenuProfileElement(ProfileSet profiles, OptionSet options, OptionValues packAppliedValues) { + this.profiles = profiles; + this.options = options; + this.packAppliedValues = packAppliedValues; + } + + /** + * @return an {@link OptionValues} that also contains values currently + * pending application. + */ + public OptionValues getPendingOptionValues() { + MutableOptionValues values = packAppliedValues.mutableCopy(); + values.addAll(Iris.getShaderPackOptionQueue()); + + return values; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuStringOptionElement.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuStringOptionElement.java new file mode 100644 index 000000000..63f27b20a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuStringOptionElement.java @@ -0,0 +1,14 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.StringOption; +import net.coderbot.iris.shaderpack.option.values.OptionValues; + +public class OptionMenuStringOptionElement extends OptionMenuOptionElement { + public final StringOption option; + + public OptionMenuStringOptionElement(String elementString, OptionMenuContainer container, ShaderProperties shaderProperties, OptionValues values, StringOption option) { + super(elementString, container, shaderProperties, values); + this.option = option; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuSubElementScreen.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuSubElementScreen.java new file mode 100644 index 000000000..dd1bd854f --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuSubElementScreen.java @@ -0,0 +1,17 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import net.coderbot.iris.shaderpack.ShaderProperties; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; + +import java.util.List; +import java.util.Optional; + +public class OptionMenuSubElementScreen extends OptionMenuElementScreen { + public final String screenId; + + public OptionMenuSubElementScreen(String screenId, OptionMenuContainer container, ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, List elementStrings, Optional columnCount) { + super(container, shaderProperties, shaderPackOptions, elementStrings, columnCount); + + this.screenId = screenId; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/values/ImmutableOptionValues.java b/src/main/java/net/coderbot/iris/shaderpack/option/values/ImmutableOptionValues.java new file mode 100644 index 000000000..1e3935fb3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/values/ImmutableOptionValues.java @@ -0,0 +1,55 @@ +package net.coderbot.iris.shaderpack.option.values; + +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.option.OptionSet; + +import java.util.HashMap; +import java.util.Optional; + +public class ImmutableOptionValues implements OptionValues { + private final OptionSet options; + private final ImmutableMap booleanValues; + private final ImmutableMap stringValues; + + ImmutableOptionValues(OptionSet options, ImmutableMap booleanValues, + ImmutableMap stringValues) { + this.options = options; + this.booleanValues = booleanValues; + this.stringValues = stringValues; + } + + @Override + public OptionalBoolean getBooleanValue(String name) { + if (booleanValues.containsKey(name)) { + return booleanValues.get(name) ? OptionalBoolean.TRUE : OptionalBoolean.FALSE; + } else { + return OptionalBoolean.DEFAULT; + } + } + + @Override + public Optional getStringValue(String name) { + return Optional.ofNullable(stringValues.get(name)); + } + + @Override + public int getOptionsChanged() { + return this.stringValues.size() + this.booleanValues.size(); + } + + @Override + public MutableOptionValues mutableCopy() { + return new MutableOptionValues(options, new HashMap<>(booleanValues), new HashMap<>(stringValues)); + } + + @Override + public ImmutableOptionValues toImmutable() { + return this; + } + + @Override + public OptionSet getOptionSet() { + return options; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/values/MutableOptionValues.java b/src/main/java/net/coderbot/iris/shaderpack/option/values/MutableOptionValues.java new file mode 100644 index 000000000..ca1a446ac --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/values/MutableOptionValues.java @@ -0,0 +1,134 @@ +package net.coderbot.iris.shaderpack.option.values; + +import com.google.common.collect.ImmutableMap; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.option.OptionSet; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class MutableOptionValues implements OptionValues { + private final OptionSet options; + private final Map booleanValues; + private final Map stringValues; + + MutableOptionValues(OptionSet options, Map booleanValues, Map stringValues) { + Map values = new HashMap<>(); + + booleanValues.forEach((k, v) -> values.put(k, Boolean.toString(v))); + values.putAll(stringValues); + + this.options = options; + this.booleanValues = new HashMap<>(); + this.stringValues = new HashMap<>(); + + this.addAll(values); + } + + public MutableOptionValues(OptionSet options, Map values) { + this.options = options; + this.booleanValues = new HashMap<>(); + this.stringValues = new HashMap<>(); + + this.addAll(values); + } + + public OptionSet getOptions() { + return options; + } + + public Map getBooleanValues() { + return booleanValues; + } + + public Map getStringValues() { + return stringValues; + } + + public void addAll(Map values) { + options.getBooleanOptions().forEach((name, option) -> { + String value = values.get(name); + OptionalBoolean booleanValue; + + if (value == null) { + return; + } + + if (value.equals("false")) { + booleanValue = OptionalBoolean.FALSE; + } else if (value.equals("true")) { + booleanValue = OptionalBoolean.TRUE; + } else { + // Invalid value specified, ignore it + // TODO: Diagnostic message? + booleanValue = OptionalBoolean.DEFAULT; + } + + boolean actualValue = booleanValue.orElse(option.getOption().getDefaultValue()); + + if (actualValue == option.getOption().getDefaultValue()) { + // Just set it to default by removing it from the map + booleanValues.remove(name); + return; + } + + booleanValues.put(name, actualValue); + }); + + options.getStringOptions().forEach((name, option) -> { + String value = values.get(name); + + if (value == null) { + return; + } + + // NB: We don't check if the option is in the allowed values here. This matches OptiFine + // behavior, the allowed values is only used when the user is changing options in the + // GUI. Profiles might specify values for options that aren't in the allowed values + // list, and values typed manually into the config .txt are unchecked. + + if (value.equals(option.getOption().getDefaultValue())) { + stringValues.remove(name); + return; + } + + stringValues.put(name, value); + }); + } + + @Override + public OptionalBoolean getBooleanValue(String name) { + if (booleanValues.containsKey(name)) { + return booleanValues.get(name) ? OptionalBoolean.TRUE : OptionalBoolean.FALSE; + } else { + return OptionalBoolean.DEFAULT; + } + } + + @Override + public Optional getStringValue(String name) { + return Optional.ofNullable(stringValues.get(name)); + } + + @Override + public int getOptionsChanged() { + return this.stringValues.size() + this.booleanValues.size(); + } + + @Override + public MutableOptionValues mutableCopy() { + return new MutableOptionValues(options, new HashMap<>(booleanValues), new HashMap<>(stringValues)); + } + + @Override + public ImmutableOptionValues toImmutable() { + return new ImmutableOptionValues(options, ImmutableMap.copyOf(booleanValues), + ImmutableMap.copyOf(stringValues)); + } + + @Override + public OptionSet getOptionSet() { + return options; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/values/OptionValues.java b/src/main/java/net/coderbot/iris/shaderpack/option/values/OptionValues.java new file mode 100644 index 000000000..1abb3f189 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/values/OptionValues.java @@ -0,0 +1,32 @@ +package net.coderbot.iris.shaderpack.option.values; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.OptionalBoolean; +import net.coderbot.iris.shaderpack.option.OptionSet; + +import java.util.Optional; + +public interface OptionValues { + OptionalBoolean getBooleanValue(String name); + Optional getStringValue(String name); + + default boolean getBooleanValueOrDefault(String name) { + return getBooleanValue(name).orElseGet(() -> { + if (!getOptionSet().getBooleanOptions().containsKey(name)) { + Iris.logger.warn("Tried to get boolean value for unknown option: " + name + ", defaulting to true!"); + return true; + } + return getOptionSet().getBooleanOptions().get(name).getOption().getDefaultValue(); + }); + } + + default String getStringValueOrDefault(String name) { + return getStringValue(name).orElseGet(() -> getOptionSet().getStringOptions().get(name).getOption().getDefaultValue()); + } + + int getOptionsChanged(); + + MutableOptionValues mutableCopy(); + ImmutableOptionValues toImmutable(); + OptionSet getOptionSet(); +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/parsing/ParsedString.java b/src/main/java/net/coderbot/iris/shaderpack/parsing/ParsedString.java new file mode 100644 index 000000000..7e96a0cef --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/parsing/ParsedString.java @@ -0,0 +1,158 @@ +package net.coderbot.iris.shaderpack.parsing; + +import org.jetbrains.annotations.Nullable; + +public class ParsedString { + private String text; + + public ParsedString(String text) { + this.text = text; + } + + public boolean takeLiteral(String token) { + if (!text.startsWith(token)) { + return false; + } + + text = text.substring(token.length()); + + return true; + } + + public boolean takeSomeWhitespace() { + if (text.isEmpty() || !Character.isWhitespace(text.charAt(0))) { + return false; + } + + // TODO: We do a double sided trim + text = text.trim(); + + return true; + } + + public boolean takeComments() { + if (!text.startsWith("//")) { + return false; + } + + // Remove the initial two comment slashes + text = text.substring(2); + + // Remove any additional comment slashes + while (text.startsWith("/")) { + text = text.substring(1); + } + + return true; + } + + public boolean currentlyContains(String text) { + return this.text.contains(text); + } + + public boolean isEnd() { + return text.isEmpty(); + } + + public String takeRest() { + return text; + } + + private String takeCharacters(int numChars) { + String result = text.substring(0, numChars); + text = text.substring(numChars); + + // TODO: Audit substring calls... + return result; + } + + @Nullable + public String takeWord() { + if (isEnd()) { + return null; + } + + int position = 0; + + for (char character : text.toCharArray()) { + if (!Character.isDigit(character) + && !Character.isAlphabetic(character) + && character != '_') { + break; + } + + position += 1; + } + + if (position == 0) { + return null; + } + + return takeCharacters(position); + } + + @Nullable + public String takeNumber() { + if (isEnd()) { + return null; + } + + int position = 0; + int digitsBefore = 0; + + if (text.charAt(0) == '-') { + position += 1; + } + + while (position < text.length() && Character.isDigit(text.charAt(position))) { + position += 1; + digitsBefore += 1; + } + + if (digitsBefore == 0) { + return null; + } + + if (position < text.length()) { + char next = text.charAt(position); + + if (next == '.') { + position += 1; + + int digitsAfter = 0; + + while (position < text.length() && Character.isDigit(text.charAt(position))) { + position += 1; + digitsAfter += 1; + } + + if (digitsAfter == 0) { + return null; + } + + if (position < text.length()) { + next = text.charAt(position); + + if (next == 'f' || next == 'F') { + position += 1; + } + } + } else if (next == 'f' || next == 'F') { + position += 1; + } + } + + return takeCharacters(position); + } + + @Nullable + public String takeWordOrNumber() { + String number = takeNumber(); + + if (number == null) { + return takeWord(); + } + + return number; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/preprocessor/GlslCollectingListener.java b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/GlslCollectingListener.java new file mode 100644 index 000000000..df3c879aa --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/GlslCollectingListener.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.shaderpack.preprocessor; + +import org.anarres.cpp.DefaultPreprocessorListener; +import org.anarres.cpp.LexerException; +import org.anarres.cpp.Source; + +public class GlslCollectingListener extends DefaultPreprocessorListener { + public static final String VERSION_MARKER = "#warning IRIS_JCPP_GLSL_VERSION"; + public static final String EXTENSION_MARKER = "#warning IRIS_JCPP_GLSL_EXTENSION"; + + private final StringBuilder builder; + + public GlslCollectingListener() { + this.builder = new StringBuilder(); + } + + @Override + public void handleWarning(Source source, int line, int column, String msg) throws LexerException { + if (msg.startsWith(VERSION_MARKER)) { + builder.append(msg.replace(VERSION_MARKER, "#version ")); + builder.append('\n'); + } else if (msg.startsWith(EXTENSION_MARKER)) { + builder.append(msg.replace(EXTENSION_MARKER, "#extension ")); + builder.append('\n'); + } else { + super.handleWarning(source, line, column, msg); + } + } + + public String collectLines() { + return builder.toString(); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/preprocessor/JcppProcessor.java b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/JcppProcessor.java new file mode 100644 index 000000000..bbc217e90 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/JcppProcessor.java @@ -0,0 +1,70 @@ +package net.coderbot.iris.shaderpack.preprocessor; + +import net.coderbot.iris.shaderpack.StringPair; +import org.anarres.cpp.Feature; +import org.anarres.cpp.LexerException; +import org.anarres.cpp.Preprocessor; +import org.anarres.cpp.StringLexerSource; +import org.anarres.cpp.Token; + +public class JcppProcessor { + // Derived from GlShader from Canvas, licenced under LGPL + public static String glslPreprocessSource(String source, Iterable environmentDefines) { + if (source.contains(GlslCollectingListener.VERSION_MARKER) + || source.contains(GlslCollectingListener.EXTENSION_MARKER)) { + throw new RuntimeException("Some shader author is trying to exploit internal Iris implementation details, stop!"); + } + + // Note: This is an absolutely awful hack. But JCPP's lack of extensibility leaves me with no choice... + // We should write our own preprocessor at some point to avoid this. + // + // Why are we doing this awful hack instead of just using the preprocessor like a normal person? Because it lets + // us only hoist #extension directives if they're actually used. This is needed for shader packs written on + // lenient drivers that allow #extension directives to be placed anywhere to work on strict drivers like Mesa + // that require #extension directives to occur at the top. + // + // TODO: This allows #version to not appear as the first non-comment non-whitespace thing in the file. + // That's not the behavior we want. If you're reading this, don't rely on this behavior. + source = source.replace("#version", GlslCollectingListener.VERSION_MARKER); + source = source.replace("#extension", GlslCollectingListener.EXTENSION_MARKER); + + GlslCollectingListener listener = new GlslCollectingListener(); + + @SuppressWarnings("resource") + final Preprocessor pp = new Preprocessor(); + + // Add the values of the environment defines without actually modifying the source code + // of the shader program, one step down the road of having accurate line number reporting + // in errors... + try { + for (StringPair envDefine : environmentDefines) { + pp.addMacro(envDefine.getKey(), envDefine.getValue()); + } + } catch (LexerException e) { + throw new RuntimeException("Unexpected LexerException processing macros", e); + } + + pp.setListener(listener); + pp.addInput(new StringLexerSource(source, true)); + pp.addFeature(Feature.KEEPCOMMENTS); + + final StringBuilder builder = new StringBuilder(); + + try { + for (;;) { + final Token tok = pp.token(); + if (tok == null) break; + if (tok.getType() == Token.EOF) break; + builder.append(tok.getText()); + } + } catch (final Exception e) { + throw new RuntimeException("GLSL source pre-processing failed", e); + } + + builder.append("\n"); + + source = listener.collectLines() + builder; + + return source; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesCommentListener.java b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesCommentListener.java new file mode 100644 index 000000000..20d33d155 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesCommentListener.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.shaderpack.preprocessor; + +import org.anarres.cpp.DefaultPreprocessorListener; +import org.anarres.cpp.LexerException; +import org.anarres.cpp.Source; + +public class PropertiesCommentListener extends DefaultPreprocessorListener { + private static final String UNKNOWN_PREPROCESSOR_DIRECTIVE = "Unknown preprocessor directive"; + private static final String NOT_A_WORD = "Preprocessor directive not a word"; + + @Override + public void handleError(Source source, int line, int column, String msg) throws LexerException { + // In .properties files, #'s are also used for comments, so ignore these errors + if (msg.contains(UNKNOWN_PREPROCESSOR_DIRECTIVE) || msg.contains(NOT_A_WORD)) { + return; + } + super.handleError(source, line, column, msg); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesPreprocessor.java b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesPreprocessor.java new file mode 100644 index 000000000..e9ae63b3c --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertiesPreprocessor.java @@ -0,0 +1,138 @@ +package net.coderbot.iris.shaderpack.preprocessor; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.StringPair; +import net.coderbot.iris.shaderpack.option.ShaderPackOptions; +import org.anarres.cpp.Feature; +import org.anarres.cpp.LexerException; +import org.anarres.cpp.Preprocessor; +import org.anarres.cpp.StringLexerSource; +import org.anarres.cpp.Token; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class PropertiesPreprocessor { + // Derived from ShaderProcessor.glslPreprocessSource, which is derived from GlShader from Canvas, licenced under LGPL + public static String preprocessSource(String source, ShaderPackOptions shaderPackOptions, Iterable environmentDefines) { + if (source.contains(PropertyCollectingListener.PROPERTY_MARKER)) { + throw new RuntimeException("Some shader author is trying to exploit internal Iris implementation details, stop!"); + } + + List booleanValues = getBooleanValues(shaderPackOptions); + Map stringValues = getStringValues(shaderPackOptions); + + try (Preprocessor pp = new Preprocessor()) { + for (String value : booleanValues) { + pp.addMacro(value); + } + + for (StringPair envDefine : environmentDefines) { + pp.addMacro(envDefine.getKey(), envDefine.getValue()); + } + + stringValues.forEach((name, value) -> { + try { + pp.addMacro(name, value); + } catch (LexerException e) { + e.printStackTrace(); + } + }); + + return process(pp, source); + } catch (IOException e) { + throw new RuntimeException("Unexpected IOException while processing macros", e); + } catch (LexerException e) { + throw new RuntimeException("Unexpected LexerException processing macros", e); + } + } + + public static String preprocessSource(String source, Iterable environmentDefines) { + if (source.contains(PropertyCollectingListener.PROPERTY_MARKER)) { + throw new RuntimeException("Some shader author is trying to exploit internal Iris implementation details, stop!"); + } + + Preprocessor preprocessor = new Preprocessor(); + + try { + for (StringPair envDefine : environmentDefines) { + preprocessor.addMacro(envDefine.getKey(), envDefine.getValue()); + } + } catch (LexerException e) { + e.printStackTrace(); + } + + return process(preprocessor, source); + } + + private static String process(Preprocessor preprocessor, String source) { + preprocessor.setListener(new PropertiesCommentListener()); + PropertyCollectingListener listener = new PropertyCollectingListener(); + preprocessor.setListener(listener); + + // Not super efficient, but this removes trailing whitespace on lines, fixing an issue with whitespace after + // line continuations (see PreprocessorTest#testWeirdPropertiesLineContinuation) + // Required for Voyager Shader + source = Arrays.stream(source.split("\\R")).map(String::trim) + .map(line -> { + if (line.startsWith("#")) { + // In PropertyCollectingListener we suppress "unknown preprocessor directive errors" and + // assume the line to be a comment, since in .properties files `#` also functions as a comment + // marker. + return line; + } else { + // This is a hack to ensure that non-macro lines don't have any preprocessing applied... + // In properties files, we don't substitute #define values except on macro lines. + return "#warning IRIS_PASSTHROUGH " + line; + } + }).collect(Collectors.joining("\n")) + "\n"; + + preprocessor.addInput(new StringLexerSource(source, true)); + preprocessor.addFeature(Feature.KEEPCOMMENTS); + + final StringBuilder builder = new StringBuilder(); + + try { + for (;;) { + final Token tok = preprocessor.token(); + if (tok == null) break; + if (tok.getType() == Token.EOF) break; + builder.append(tok.getText()); + } + } catch (final Exception e) { + Iris.logger.error("Properties pre-processing failed", e); + } + + source = builder.toString(); + + return listener.collectLines() + source; + } + + private static List getBooleanValues(ShaderPackOptions shaderPackOptions) { + List booleanValues = new ArrayList<>(); + + shaderPackOptions.getOptionSet().getBooleanOptions().forEach((string, value) -> { + boolean trueValue = shaderPackOptions.getOptionValues().getBooleanValueOrDefault(string); + + if (trueValue) { + booleanValues.add(string); + } + }); + + return booleanValues; + } + + private static Map getStringValues(ShaderPackOptions shaderPackOptions) { + Map stringValues = new HashMap<>(); + + shaderPackOptions.getOptionSet().getStringOptions().forEach( + (optionName, value) -> stringValues.put(optionName, shaderPackOptions.getOptionValues().getStringValueOrDefault(optionName))); + + return stringValues; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertyCollectingListener.java b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertyCollectingListener.java new file mode 100644 index 000000000..031ea6183 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/preprocessor/PropertyCollectingListener.java @@ -0,0 +1,40 @@ +package net.coderbot.iris.shaderpack.preprocessor; + +import org.anarres.cpp.DefaultPreprocessorListener; +import org.anarres.cpp.LexerException; +import org.anarres.cpp.Source; + +public class PropertyCollectingListener extends DefaultPreprocessorListener { + public static final String PROPERTY_MARKER = "#warning IRIS_PASSTHROUGH "; + + private final StringBuilder builder; + + public PropertyCollectingListener() { + this.builder = new StringBuilder(); + } + + @Override + public void handleWarning(Source source, int line, int column, String msg) throws LexerException { + if (msg.startsWith(PROPERTY_MARKER)) { + builder.append(msg.replace(PROPERTY_MARKER, "")); + builder.append('\n'); + } else { + super.handleWarning(source, line, column, msg); + } + } + + @Override + public void handleError(Source source, int line, int column, String msg) throws LexerException { + if (msg.contains("Unknown preprocessor directive") + || msg.contains("Preprocessor directive not a word")) { + // Suppress log spam since hashed lines also function as comments in preprocessed files. + return; + } + + super.handleError(source, line, column, msg); + } + + public String collectLines() { + return builder.toString(); + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/texture/CustomTextureData.java b/src/main/java/net/coderbot/iris/shaderpack/texture/CustomTextureData.java new file mode 100644 index 000000000..3dda4ea30 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/texture/CustomTextureData.java @@ -0,0 +1,161 @@ +package net.coderbot.iris.shaderpack.texture; + +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.gl.texture.PixelFormat; +import net.coderbot.iris.gl.texture.PixelType; + +public abstract class CustomTextureData { + private CustomTextureData() { + + } + + public static final class PngData extends CustomTextureData { + private final TextureFilteringData filteringData; + private final byte[] content; + + public PngData(TextureFilteringData filteringData, byte[] content) { + this.filteringData = filteringData; + this.content = content; + } + + public TextureFilteringData getFilteringData() { + return filteringData; + } + + public byte[] getContent() { + return content; + } + } + + public static final class LightmapMarker extends CustomTextureData { + @Override + public boolean equals(Object obj) { + return obj.getClass() == this.getClass(); + } + + @Override + public int hashCode() { + return 33; + } + } + + public static final class ResourceData extends CustomTextureData { + private final String namespace; + private final String location; + + public ResourceData(String namespace, String location) { + this.namespace = namespace; + this.location = location; + } + + /** + * @return The namespace of the texture. The caller is responsible for checking whether this is actually + * a valid namespace. + */ + public String getNamespace() { + return namespace; + } + + /** + * @return The path / location of the texture. The caller is responsible for checking whether this is actually + * a valid path. + */ + public String getLocation() { + return location; + } + } + + public abstract static class RawData extends CustomTextureData { + private final byte[] content; + private final InternalTextureFormat internalFormat; + private final PixelFormat pixelFormat; + private final PixelType pixelType; + + private RawData(byte[] content, InternalTextureFormat internalFormat, + PixelFormat pixelFormat, PixelType pixelType) { + this.content = content; + this.internalFormat = internalFormat; + this.pixelFormat = pixelFormat; + this.pixelType = pixelType; + } + + public final byte[] getContent() { + return content; + } + + public final InternalTextureFormat getInternalFormat() { + return internalFormat; + } + + public final PixelFormat getPixelFormat() { + return pixelFormat; + } + + public final PixelType getPixelType() { + return pixelType; + } + } + + public static final class RawData1D extends RawData { + private final int sizeX; + + private RawData1D(byte[] content, InternalTextureFormat internalFormat, + PixelFormat pixelFormat, PixelType pixelType, int sizeX) { + super(content, internalFormat, pixelFormat, pixelType); + + this.sizeX = sizeX; + } + + public int getSizeX() { + return sizeX; + } + } + + public static final class RawData2D extends RawData { + int sizeX; + int sizeY; + + private RawData2D(byte[] content, InternalTextureFormat internalFormat, + PixelFormat pixelFormat, PixelType pixelType, int sizeX, int sizeY) { + super(content, internalFormat, pixelFormat, pixelType); + + this.sizeX = sizeX; + this.sizeY = sizeY; + } + + public int getSizeX() { + return sizeX; + } + + public int getSizeY() { + return sizeY; + } + } + + public static final class RawData3D extends RawData { + int sizeX; + int sizeY; + int sizeZ; + + private RawData3D(byte[] content, InternalTextureFormat internalFormat, + PixelFormat pixelFormat, PixelType pixelType, int sizeX, int sizeY, int sizeZ) { + super(content, internalFormat, pixelFormat, pixelType); + + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + } + + public int getSizeX() { + return sizeX; + } + + public int getSizeY() { + return sizeY; + } + + public int getSizeZ() { + return sizeZ; + } + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/texture/TextureFilteringData.java b/src/main/java/net/coderbot/iris/shaderpack/texture/TextureFilteringData.java new file mode 100644 index 000000000..55ea5bc9a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/texture/TextureFilteringData.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.shaderpack.texture; + +public final class TextureFilteringData { + private final boolean blur; + private final boolean clamp; + + public TextureFilteringData(boolean blur, boolean clamp) { + this.blur = blur; + this.clamp = clamp; + } + + public boolean shouldBlur() { + return blur; + } + + public boolean shouldClamp() { + return clamp; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/texture/TextureStage.java b/src/main/java/net/coderbot/iris/shaderpack/texture/TextureStage.java new file mode 100644 index 000000000..3fe872988 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/texture/TextureStage.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.shaderpack.texture; + +import java.util.Optional; + +public enum TextureStage { + /** + * The shadowcomp passes. + * + * While this is not documented in shaders.txt, it is a valid stage for defining custom textures. + */ + SHADOWCOMP, + /** + * The prepare passes. + * + * While this is not documented in shaders.txt, it is a valid stage for defining custom textures. + */ + PREPARE, + /** + * All of the gbuffer passes, as well as the shadow passes. + */ + GBUFFERS_AND_SHADOW, + /** + * The deferred pass. + */ + DEFERRED, + /** + * The composite pass and final pass. + */ + COMPOSITE_AND_FINAL; + + public static Optional parse(String name) { + return switch (name) { + case "shadowcomp" -> Optional.of(SHADOWCOMP); + case "prepare" -> Optional.of(PREPARE); + case "gbuffers" -> Optional.of(GBUFFERS_AND_SHADOW); + case "deferred" -> Optional.of(DEFERRED); + case "composite" -> Optional.of(COMPOSITE_AND_FINAL); + default -> Optional.empty(); + }; + } +} diff --git a/src/main/java/net/coderbot/iris/shaderpack/transform/StringTransformations.java b/src/main/java/net/coderbot/iris/shaderpack/transform/StringTransformations.java new file mode 100644 index 000000000..a0c81331a --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/transform/StringTransformations.java @@ -0,0 +1,128 @@ +package net.coderbot.iris.shaderpack.transform; + +public class StringTransformations implements Transformations { + private String prefix; + private String extensions; + private StringBuilder injections; + private String body; + private StringBuilder suffix; + + public StringTransformations(String base) { + int versionStringStart = base.indexOf("#version"); + + if (versionStringStart == -1) { + throw new IllegalArgumentException("A valid shader should include a version string"); + } + + String prefix = base.substring(0, versionStringStart); + base = base.substring(versionStringStart); + + int splitPoint = base.indexOf("\n") + 1; + + this.prefix = prefix + base.substring(0, splitPoint); + this.extensions = ""; + this.injections = new StringBuilder(); + this.body = base.substring(splitPoint); + this.suffix = new StringBuilder("\n"); + + if (!body.contains("#extension")) { + // Don't try to hoist #extension lines if there are none. + return; + } + + // We need to avoid injecting non-preprocessor code fragments before #extension + // declarations. Luckily, JCPP hoists #extension directives to be right after #version + // directives. + StringBuilder extensions = new StringBuilder(); + StringBuilder body = new StringBuilder(); + + boolean inBody = false; + + for (String line : this.body.split("\\R")) { + String trimmedLine = line.trim(); + + if (!trimmedLine.isEmpty() + && !trimmedLine.startsWith("#extension") + && !trimmedLine.startsWith("//")) { + inBody = true; + } + + if (inBody) { + body.append(line); + body.append('\n'); + } else { + extensions.append(line); + extensions.append('\n'); + } + } + + this.extensions = extensions.toString(); + this.body = body.toString(); + } + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean contains(String content) { + return toString().contains(content); + } + + @Override + public void define(String key, String value) { + // TODO: This isn't super efficient, but oh well. + extensions = extensions + "#define " + key + " " + value + "\n"; + } + + @Override + public void injectLine(InjectionPoint at, String line) { + if (at == InjectionPoint.BEFORE_CODE) { + injections.append(line); + injections.append('\n'); + } else if (at == InjectionPoint.DEFINES) { + // TODO: This isn't super efficient, but oh well. + extensions = extensions + line + "\n"; + } else if (at == InjectionPoint.END) { + suffix.append(line); + suffix.append('\n'); + } else { + throw new IllegalArgumentException("Unsupported injection point: " + at); + } + } + + @Override + public void replaceExact(String from, String to) { + if (from.contains("\n")) { + // Block newline replacements for now, since that could mean that we're trying to replace across + // injections / body and that will result in weird behavior. + throw new UnsupportedOperationException(); + } + + prefix = prefix.replace(from, to); + extensions = extensions.replace(from, to); + injections = new StringBuilder(injections.toString().replace(from, to)); + body = body.replace(from, to); + suffix = new StringBuilder(suffix.toString().replace(from, to)); + } + + @Override + public void replaceRegex(String regex, String to) { + prefix = prefix.replaceAll(regex, to); + extensions = extensions.replaceAll(regex, to); + injections = new StringBuilder(injections.toString().replaceAll(regex, to)); + body = body.replaceAll(regex, to); + suffix = new StringBuilder(suffix.toString().replaceAll(regex, to)); + } + + @Override + public String toString() { + return prefix + extensions + injections + body + suffix; + } +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/shaderpack/transform/Transformations.java b/src/main/java/net/coderbot/iris/shaderpack/transform/Transformations.java new file mode 100644 index 000000000..82af8d9ae --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/transform/Transformations.java @@ -0,0 +1,17 @@ +package net.coderbot.iris.shaderpack.transform; + +public interface Transformations { + boolean contains(String content); + void injectLine(InjectionPoint at, String line); + void replaceExact(String from, String to); + void replaceRegex(String regex, String to); + String getPrefix(); + void setPrefix(String prefix); + void define(String key, String value); + + enum InjectionPoint { + DEFINES, + BEFORE_CODE, + END + } +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/shaderpack/transform/line/LineTransform.java b/src/main/java/net/coderbot/iris/shaderpack/transform/line/LineTransform.java new file mode 100644 index 000000000..159526825 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/transform/line/LineTransform.java @@ -0,0 +1,19 @@ +package net.coderbot.iris.shaderpack.transform.line; + +import com.google.common.collect.ImmutableList; + +public interface LineTransform { + String transform(int index, String line); + + static ImmutableList apply(ImmutableList lines, LineTransform transform) { + ImmutableList.Builder newLines = ImmutableList.builder(); + int index = 0; + + for (String line : lines) { + newLines.add(transform.transform(index, line)); + index += 1; + } + + return newLines.build(); + } +} diff --git a/src/main/java/net/coderbot/iris/shadow/ShadowMatrices.java b/src/main/java/net/coderbot/iris/shadow/ShadowMatrices.java new file mode 100644 index 000000000..7142d5e73 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadow/ShadowMatrices.java @@ -0,0 +1,175 @@ +package net.coderbot.iris.shadow; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import org.joml.Matrix4f; + +import static com.gtnewhorizons.angelica.compat.mojang.Constants.DEGREES_TO_RADIANS; + +public class ShadowMatrices { + private static final float NEAR = 0.05f; + private static final float FAR = 256.0f; + + // NB: These matrices are in column-major order, not row-major order like what you'd expect! + + public static Matrix4f createOrthoMatrix(float halfPlaneLength) { + return new Matrix4f( + // column 1 + 1.0f / halfPlaneLength, 0f, 0f, 0f, + // column 2 + 0f, 1.0f / halfPlaneLength, 0f, 0f, + // column 3 + 0f, 0f, 2.0f / (NEAR - FAR), 0f, + // column 4 + 0f, 0f, -(FAR + NEAR) / (FAR - NEAR), 1f + ); + } + + public static Matrix4f createPerspectiveMatrix(float fov) { + // This converts from degrees to radians. + final float yScale = (float) (1.0f / Math.tan(Math.toRadians(fov) * 0.5f)); + return new Matrix4f( + // column 1 + yScale, 0f, 0f, 0f, + // column 2 + 0f, yScale, 0f, 0f, + // column 3 + 0f, 0f, (FAR + NEAR) / (NEAR - FAR), -1.0F, + // column 4 + 0f, 0f, 2.0F * FAR * NEAR / (NEAR - FAR), 1f + ); + } + + public static void createBaselineModelViewMatrix(MatrixStack target, float shadowAngle, float sunPathRotation) { + final float skyAngle; + + if (shadowAngle < 0.25f) { + skyAngle = shadowAngle + 0.75f; + } else { + skyAngle = shadowAngle - 0.25f; + } + + target.peek().getNormal().identity(); + target.peek().getModel().identity(); + + target.peek().getModel().translate(0.0f, 0.0f, -100.0f); + target.rotateX(90F * DEGREES_TO_RADIANS); + target.rotateZ(skyAngle * -360.0f * DEGREES_TO_RADIANS); + target.rotateX(sunPathRotation * DEGREES_TO_RADIANS); + } + + public static void snapModelViewToGrid(MatrixStack target, float shadowIntervalSize, double cameraX, double cameraY, double cameraZ) { + if (Math.abs(shadowIntervalSize) == 0.0F) { + // Avoid a division by zero - semantically, this just means that the snapping does not take place, + // if the shadow interval (size of each grid "cell") is zero. + return; + } + + // Calculate where we are within each grid "cell" + // These values will be in the range of (-shadowIntervalSize, shadowIntervalSize) + // + // It looks like it's intended for these to be within the range [0, shadowIntervalSize), however since the + // expression (-2.0f % 32.0f) returns -2.0f, negative inputs will result in negative outputs. + float offsetX = (float) cameraX % shadowIntervalSize; + float offsetY = (float) cameraY % shadowIntervalSize; + float offsetZ = (float) cameraZ % shadowIntervalSize; + + // Halve the size of each grid cell in order to move to the center of it. + final float halfIntervalSize = shadowIntervalSize / 2.0f; + + // Shift by -halfIntervalSize + // + // It's clear that the intent of the algorithm was to place the values into the range: + // [-shadowIntervalSize/2, shadowIntervalSize), however due to the previously-mentioned behavior with negatives, + // it's possible that values will lie in the range (-3shadowIntervalSize/2, shadowIntervalSize/2). + offsetX -= halfIntervalSize; + offsetY -= halfIntervalSize; + offsetZ -= halfIntervalSize; + + target.peek().getModel().translate(offsetX, offsetY, offsetZ); + } + + public static void createModelViewMatrix(MatrixStack target, float shadowAngle, float shadowIntervalSize, float sunPathRotation, double cameraX, double cameraY, double cameraZ) { + createBaselineModelViewMatrix(target, shadowAngle, sunPathRotation); + snapModelViewToGrid(target, shadowIntervalSize, cameraX, cameraY, cameraZ); + } + + private static final class Tests { + public static void main(String[] args) { + // const float shadowDistance = 32.0; + // /* SHADOWHPL:32.0 */ + Matrix4f expected = new Matrix4f( + 0.03125f, 0f, 0f, 0f, + 0f, 0.03125f, 0f, 0f, + 0f, 0f, -0.007814026437699795f, 0f, + 0f, 0f, -1.000390648841858f, 1f + ); + + test("ortho projection hpl=32", expected, createOrthoMatrix(32.0f)); + + // const float shadowDistance = 110.0; + // /* SHADOWHPL:110.0 */ + Matrix4f expected110 = new Matrix4f( + 0.00909090880304575f, 0, 0, 0, + 0, 0.00909090880304575f, 0, 0, + 0, 0, -0.007814026437699795f, 0, + 0, 0, -1.000390648841858f, 1 + ); + + test("ortho projection hpl=110", expected110, createOrthoMatrix(110.0f)); + + Matrix4f expected90Proj = new Matrix4f( + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, -1.0003906f, -1.0f, + 0.0f, 0.0f, -0.10001954f, 0.0f + ); + + test("perspective projection fov=90", expected90Proj, createPerspectiveMatrix(90.0f)); + + Matrix4f expectedModelViewAtDawn = new Matrix4f( + // column 1 + 0.21545040607452393f, + 5.820481518981069E-8f, + 0.9765146970748901f, + 0, + // column 2 + -0.9765147466795349f, + 1.2841844920785661E-8f, + 0.21545039117336273f, + 0, + // column 3 + 0, + -0.9999999403953552f, + 5.960464477539063E-8f, + 0, + // column 4 + 0.38002151250839233f, + 1.0264281034469604f, + -100.4463119506836f, + 1 + ); + + MatrixStack modelView = new MatrixStack(); + + // NB: At dawn, the shadow angle is NOT zero. + // When DayTime=0, skyAngle = 282 degrees. + // Thus, sunAngle = shadowAngle = 0.03451777f + createModelViewMatrix(modelView, 0.03451777f, 2.0f, + 0.0f, 0.646045982837677f, 82.53274536132812f, -514.0264282226562f); + + test("model view at dawn", expectedModelViewAtDawn, modelView.peek().getModel()); + } + + private static void test(String name, Matrix4f expected, Matrix4f created) { + if (expected.equals(created, 0.0005f)) { + System.err.println("test " + name + " failed: "); + System.err.println(" expected: "); + System.err.print(expected.toString()); + System.err.println(" created: "); + System.err.print(created.toString()); + } else { + System.out.println("test " + name + " passed"); + } + } + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/CullingDataCache.java b/src/main/java/net/coderbot/iris/shadows/CullingDataCache.java new file mode 100644 index 000000000..010579561 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/CullingDataCache.java @@ -0,0 +1,6 @@ +package net.coderbot.iris.shadows; + +public interface CullingDataCache { + void saveState(); + void restoreState(); +} diff --git a/src/main/java/net/coderbot/iris/shadows/Matrix4fAccess.java b/src/main/java/net/coderbot/iris/shadows/Matrix4fAccess.java new file mode 100644 index 000000000..c0e0b9f24 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/Matrix4fAccess.java @@ -0,0 +1,25 @@ +package net.coderbot.iris.shadows; + +import org.joml.Matrix4f; + +public interface Matrix4fAccess { + /** + * Sets the values of this matrix from an array. The values in the array must be specified in column-major order, + * just like with OpenGL. Keep this in mind, since the natural way of laying out a matrix in array form is row-major + * order! + */ + void copyFromArray(float[] m); + + /** + * Gets the values of this matrix into an array. The values in the array will be laid out in column-major order, + * just like with OpenGL. Keep this in mind, since the natural way of laying out a matrix in array form is row-major + * order! + */ + float[] copyIntoArray(); + + /** + * Converts the matrix into a JOML matrix. This matrix is inherently column-major, and compatible with OpenGL. + * @return JOML matrix + */ + Matrix4f convertToJOML(); +} diff --git a/src/main/java/net/coderbot/iris/shadows/ShadowRenderTargets.java b/src/main/java/net/coderbot/iris/shadows/ShadowRenderTargets.java new file mode 100644 index 000000000..767b18cd1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/ShadowRenderTargets.java @@ -0,0 +1,274 @@ +package net.coderbot.iris.shadows; + +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.texture.DepthBufferFormat; +import net.coderbot.iris.gl.texture.DepthCopyStrategy; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.rendertarget.DepthTexture; +import net.coderbot.iris.rendertarget.RenderTarget; +import net.coderbot.iris.shaderpack.PackShadowDirectives; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +public class ShadowRenderTargets { + private final RenderTarget[] targets; + private final DepthTexture mainDepth; + private final DepthTexture noTranslucents; + private final GlFramebuffer depthSourceFb; + private final GlFramebuffer noTranslucentsDestFb; + private final boolean[] flipped; + + private final List ownedFramebuffers; + private final int resolution; + + private boolean fullClearRequired; + private boolean translucentDepthDirty; + private boolean[] hardwareFiltered; + private InternalTextureFormat[] formats; + private IntList buffersToBeCleared; + + public ShadowRenderTargets(int resolution, PackShadowDirectives shadowDirectives) { + targets = new RenderTarget[shadowDirectives.getColorSamplingSettings().size()]; + formats = new InternalTextureFormat[shadowDirectives.getColorSamplingSettings().size()]; + flipped = new boolean[shadowDirectives.getColorSamplingSettings().size()]; + hardwareFiltered = new boolean[shadowDirectives.getColorSamplingSettings().size()]; + buffersToBeCleared = new IntArrayList(); + + this.mainDepth = new DepthTexture(resolution, resolution, DepthBufferFormat.DEPTH); + this.noTranslucents = new DepthTexture(resolution, resolution, DepthBufferFormat.DEPTH); + + for (int i = 0; i < shadowDirectives.getColorSamplingSettings().size(); i++) { + PackShadowDirectives.SamplingSettings settings = shadowDirectives.getColorSamplingSettings().get(i); + targets[i] = RenderTarget.builder().setDimensions(resolution, resolution) + .setInternalFormat(settings.getFormat()) + .setPixelFormat(settings.getFormat().getPixelFormat()).build(); + formats[i] = settings.getFormat(); + + if (settings.getClear()) { + buffersToBeCleared.add(i); + } + + this.hardwareFiltered[i] = shadowDirectives.getDepthSamplingSettings().get(i).getHardwareFiltering(); + } + + this.resolution = resolution; + + this.ownedFramebuffers = new ArrayList<>(); + + // NB: Make sure all buffers are cleared so that they don't contain undefined + // data. Otherwise very weird things can happen. + fullClearRequired = true; + + this.depthSourceFb = createFramebufferWritingToMain(new int[] {0}); + + this.noTranslucentsDestFb = createFramebufferWritingToMain(new int[] {0}); + this.noTranslucentsDestFb.addDepthAttachment(this.noTranslucents.getTextureId()); + + this.translucentDepthDirty = true; + } + + // TODO: Actually flip. This is required for shadow composites! + public void flip(int target) { + flipped[target] = !flipped[target]; + } + + public boolean isFlipped(int target) { + return flipped[target]; + } + + public void destroy() { + for (GlFramebuffer owned : ownedFramebuffers) { + owned.destroy(); + } + + for (RenderTarget target : targets) { + target.destroy(); + } + + noTranslucents.destroy(); + } + + public int getRenderTargetCount() { + return targets.length; + } + + public RenderTarget get(int index) { + return targets[index]; + } + + public int getResolution() { + return resolution; + } + + public DepthTexture getDepthTexture() { + return mainDepth; + } + + public DepthTexture getDepthTextureNoTranslucents() { + return noTranslucents; + } + + public GlFramebuffer getDepthSourceFb() { + return depthSourceFb; + } + + public void copyPreTranslucentDepth() { + if (translucentDepthDirty) { + translucentDepthDirty = false; + IrisRenderSystem.blitFramebuffer(depthSourceFb.getId(), noTranslucentsDestFb.getId(), 0, 0, resolution, resolution, + 0, 0, resolution, resolution, GL11.GL_DEPTH_BUFFER_BIT, GL11.GL_NEAREST); + } else { + DepthCopyStrategy.fastest(false).copy(depthSourceFb, mainDepth.getTextureId(), noTranslucentsDestFb, noTranslucents.getTextureId(), + resolution, resolution); + } + } + + public boolean isFullClearRequired() { + return fullClearRequired; + } + + public void onFullClear() { + fullClearRequired = false; + } + + public GlFramebuffer createFramebufferWritingToMain(int[] drawBuffers) { + return createFullFramebuffer(false, drawBuffers); + } + + public GlFramebuffer createFramebufferWritingToAlt(int[] drawBuffers) { + return createFullFramebuffer(true, drawBuffers); + } + + private ImmutableSet invert(ImmutableSet base, int[] relevant) { + ImmutableSet.Builder inverted = ImmutableSet.builder(); + + for (int i : relevant) { + if (!base.contains(i)) { + inverted.add(i); + } + } + + return inverted.build(); + } + + private GlFramebuffer createEmptyFramebuffer() { + GlFramebuffer framebuffer = new GlFramebuffer(); + ownedFramebuffers.add(framebuffer); + + framebuffer.addDepthAttachment(mainDepth.getTextureId()); + + // NB: Before OpenGL 3.0, all framebuffers are required to have a color + // attachment no matter what. + framebuffer.addColorAttachment(0, get(0).getMainTexture()); + framebuffer.noDrawBuffers(); + + return framebuffer; + } + + public GlFramebuffer createShadowFramebuffer(ImmutableSet stageWritesToAlt, int[] drawBuffers) { + if (drawBuffers.length == 0) { + return createEmptyFramebuffer(); + } + + ImmutableSet stageWritesToMain = invert(stageWritesToAlt, drawBuffers); + GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); + framebuffer.addDepthAttachment(mainDepth.getTextureId()); + + return framebuffer; + } + + private GlFramebuffer createFullFramebuffer(boolean clearsAlt, int[] drawBuffers) { + if (drawBuffers.length == 0) { + return createEmptyFramebuffer(); + } + + ImmutableSet stageWritesToMain = ImmutableSet.of(); + + if (!clearsAlt) { + stageWritesToMain = invert(ImmutableSet.of(), drawBuffers); + } + + return createColorFramebufferWithDepth(stageWritesToMain, drawBuffers); + } + + public GlFramebuffer createColorFramebufferWithDepth(ImmutableSet stageWritesToMain, int[] drawBuffers) { + GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); + + framebuffer.addDepthAttachment(mainDepth.getTextureId()); + + return framebuffer; + } + + public GlFramebuffer createColorFramebuffer(ImmutableSet stageWritesToMain, int[] drawBuffers) { + if (drawBuffers.length == 0) { + throw new IllegalArgumentException("Framebuffer must have at least one color buffer"); + } + + GlFramebuffer framebuffer = new GlFramebuffer(); + ownedFramebuffers.add(framebuffer); + + int[] actualDrawBuffers = new int[drawBuffers.length]; + + for (int i = 0; i < drawBuffers.length; i++) { + actualDrawBuffers[i] = i; + + if (drawBuffers[i] >= getRenderTargetCount()) { + // TODO: This causes resource leaks, also we should really verify this in the shaderpack parser... + throw new IllegalStateException("Render target with index " + drawBuffers[i] + " is not supported, only " + + getRenderTargetCount() + " render targets are supported."); + } + + RenderTarget target = this.get(drawBuffers[i]); + + int textureId = stageWritesToMain.contains(drawBuffers[i]) ? target.getMainTexture() : target.getAltTexture(); + + framebuffer.addColorAttachment(i, textureId); + } + + framebuffer.drawBuffers(actualDrawBuffers); + framebuffer.readBuffer(0); + + if (!framebuffer.isComplete()) { + throw new IllegalStateException("Unexpected error while creating framebuffer"); + } + + return framebuffer; + } + + public int getColorTextureId(int i) { + return isFlipped(i) ? get(i).getAltTexture() : get(i).getMainTexture(); + } + + public boolean isHardwareFiltered(int i) { + return hardwareFiltered[i]; + } + + public int getNumColorTextures() { + return targets.length; + } + + public InternalTextureFormat getColorTextureFormat(int index) { + return formats[index]; + } + + public ImmutableSet snapshot() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (int i = 0; i < flipped.length; i++) { + if (flipped[i]) { + builder.add(i); + } + } + + return builder.build(); + } + + public IntList getBuffersToBeCleared() { + return buffersToBeCleared; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/ShadowRenderingState.java b/src/main/java/net/coderbot/iris/shadows/ShadowRenderingState.java new file mode 100644 index 000000000..04412a398 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/ShadowRenderingState.java @@ -0,0 +1,14 @@ +package net.coderbot.iris.shadows; + +import net.coderbot.iris.pipeline.ShadowRenderer; +import org.joml.Matrix4f; + +public class ShadowRenderingState { + public static boolean areShadowsCurrentlyBeingRendered() { + return ShadowRenderer.ACTIVE; + } + + public static Matrix4f getShadowOrthoMatrix() { + return ShadowRenderer.ACTIVE ? new Matrix4f(ShadowRenderer.PROJECTION) : null; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/BoxCuller.java b/src/main/java/net/coderbot/iris/shadows/frustum/BoxCuller.java new file mode 100644 index 000000000..48a6614f6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/BoxCuller.java @@ -0,0 +1,48 @@ +package net.coderbot.iris.shadows.frustum; + +import net.minecraft.util.AxisAlignedBB; + +public class BoxCuller { + private final double maxDistance; + + private double minAllowedX; + private double maxAllowedX; + private double minAllowedY; + private double maxAllowedY; + private double minAllowedZ; + private double maxAllowedZ; + + public BoxCuller(double maxDistance) { + this.maxDistance = maxDistance; + } + + public void setPosition(double cameraX, double cameraY, double cameraZ) { + this.minAllowedX = cameraX - maxDistance; + this.maxAllowedX = cameraX + maxDistance; + this.minAllowedY = cameraY - maxDistance; + this.maxAllowedY = cameraY + maxDistance; + this.minAllowedZ = cameraZ - maxDistance; + this.maxAllowedZ = cameraZ + maxDistance; + } + + public boolean isCulled(AxisAlignedBB aabb) { + return isCulled((float) aabb.minX, (float) aabb.minY, (float) aabb.minZ, + (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ); + } + + public boolean isCulled(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + if (maxX < this.minAllowedX || minX > this.maxAllowedX) { + return true; + } + + if (maxY < this.minAllowedY || minY > this.maxAllowedY) { + return true; + } + + if (maxZ < this.minAllowedZ || minZ > this.maxAllowedZ) { + return true; + } + + return false; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/CullEverythingFrustum.java b/src/main/java/net/coderbot/iris/shadows/frustum/CullEverythingFrustum.java new file mode 100644 index 000000000..8cbef5a5c --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/CullEverythingFrustum.java @@ -0,0 +1,23 @@ +package net.coderbot.iris.shadows.frustum; + +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.util.AxisAlignedBB; + +public class CullEverythingFrustum extends Frustrum { + + // for Sodium + public boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return false; + } + + // For Immersive Portals + // We return false here since isVisible is going to return false anyways. + public boolean canDetermineInvisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return false; + } + + @Override + public boolean isBoundingBoxInFrustum(AxisAlignedBB aabb) { + return false; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/FrustumHolder.java b/src/main/java/net/coderbot/iris/shadows/frustum/FrustumHolder.java new file mode 100644 index 000000000..701bb70c1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/FrustumHolder.java @@ -0,0 +1,28 @@ +package net.coderbot.iris.shadows.frustum; + +import net.minecraft.client.renderer.culling.Frustrum; + +public class FrustumHolder { + private Frustrum frustum; + private String distanceInfo = "(unavailable)"; + private String cullingInfo = "(unavailable)"; + + public FrustumHolder setInfo(Frustrum frustum, String distanceInfo, String cullingInfo) { + this.frustum = frustum; + this.distanceInfo = distanceInfo; + this.cullingInfo = cullingInfo; + return this; + } + + public Frustrum getFrustum() { + return frustum; + } + + public String getDistanceInfo() { + return distanceInfo; + } + + public String getCullingInfo() { + return cullingInfo; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/advanced/AdvancedShadowCullingFrustum.java b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/AdvancedShadowCullingFrustum.java new file mode 100644 index 000000000..43d843c22 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/AdvancedShadowCullingFrustum.java @@ -0,0 +1,371 @@ +package net.coderbot.iris.shadows.frustum.advanced; + +import net.coderbot.iris.shadows.frustum.BoxCuller; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.util.AxisAlignedBB; +import org.joml.Math; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +/** + * A Frustum implementation that derives a tightly-fitted shadow pass frustum based on the player's camera frustum and + * an assumption that the shadow map will only be sampled for the purposes of direct shadow casting, volumetric lighting, + * and similar effects, but notably not sun-bounce GI or similar effects. + * + *

The key idea of this algorithm is that if you are looking at the sun, something behind you cannot directly cast + * a shadow on things visible to you. It's clear why this wouldn't work for sun-bounce GI, since with sun-bounce GI an + * object behind you could cause light to bounce on to things visible to you.

+ * + *

Derived from L. Spiro's clever algorithm & helpful diagrams described in a two-part blog tutorial:

+ * + * + * + *

Notable changes include switching out some of the sub-algorithms for computing the "extruded" edge planes to ones that + * are not sensitive to the specific internal ordering of planes and corners, in order to avoid potential bugs at the + * cost of slightly more computations.

+ */ +public class AdvancedShadowCullingFrustum extends Frustrum { + private static final int MAX_CLIPPING_PLANES = 13; + + /** + * We store each plane equation as a Vector4f. + * + *

We can represent a plane equation of the form ax + by + cz = d as a 4-dimensional vector + * of the form (a, b, c, -d). In the special case of a plane that intersects the origin, we get + * the 4-dimensional vector (a, b, c, 0). (a, b, c) is the normal vector of the plane, and d is the + * distance of the plane from the origin along that normal vector.

+ * + *

Then, to test a given point (x, y, z) against the plane, we simply extend that point to a 4-component + * homogenous vector (x, y, z, 1), and then compute the dot product. Computing the dot product gives us + * ax + by + cz - d = 0, or, rearranged, our original plane equation of ax = by + cz = d.

+ * + *

Note that, for the purposes of frustum culling, we usually aren't interested in computing whether a point + * lies exactly on a plane. Rather, we are interested in determining which side of the plane the point exists on + * - the side closer to the origin, or the side farther away from the origin. Fortunately, doing this with the + * dot product is still simple. If the dot product is negative, then the point lies closer to the origin than the + * plane, and if the dot product is positive, then the point lies further from the origin than the plane.

+ * + *

In this case, if the point is closer to the origin than the plane, it is outside of the area enclosed by the + * plane, and if the point is farther from the origin than the plane, it is inside the area enclosed by the plane.

+ * + *

So: + *

    + *
  • dot(plane, point) > 0 implies the point is inside
  • + *
  • dot(plane, point) < 0 implies that the point is outside
  • + *
+ *

+ */ + private final Vector4f[] planes = new Vector4f[MAX_CLIPPING_PLANES]; + private int planeCount = 0; + + // The center coordinates of this frustum. + private double x; + private double y; + private double z; + + private final Vector3f shadowLightVectorFromOrigin; + private final BoxCuller boxCuller; + + public AdvancedShadowCullingFrustum(Matrix4f playerView, Matrix4f playerProjection, Vector3f shadowLightVectorFromOrigin, + BoxCuller boxCuller) { + this.shadowLightVectorFromOrigin = shadowLightVectorFromOrigin; + BaseClippingPlanes baseClippingPlanes = new BaseClippingPlanes(playerView, playerProjection); + + boolean[] isBack = addBackPlanes(baseClippingPlanes); + addEdgePlanes(baseClippingPlanes, isBack); + + this.boxCuller = boxCuller; + } + + private void addPlane(Vector4f plane) { + planes[planeCount] = plane; + planeCount += 1; + } + + /** + * Adds the back planes of the player's view frustum from the perspective of the shadow light. + * This can eliminate many chunks, especially if the player is staring at the shadow light + * (sun / moon). + */ + private boolean[] addBackPlanes(BaseClippingPlanes baseClippingPlanes) { + Vector4f[] planes = baseClippingPlanes.getPlanes(); + boolean[] isBack = new boolean[planes.length]; + + for (int planeIndex = 0; planeIndex < planes.length; planeIndex++) { + Vector4f plane = planes[planeIndex]; + Vector3f planeNormal = truncate(plane); + + // Find back planes by looking for planes with a normal vector that points + // in the same general direction as the vector pointing from the origin to the shadow light + // + // That is, the angle between those two vectors is less than or equal to 90 degrees, + // meaning that the dot product is positive or zero. + + float dot = planeNormal.dot(shadowLightVectorFromOrigin); + + boolean back = dot > 0.0; + boolean edge = dot == 0.0; + + // TODO: audit behavior when the dot product is zero + isBack[planeIndex] = back; + + if (back || edge) { + addPlane(plane); + } + } + + return isBack; + } + + private void addEdgePlanes(BaseClippingPlanes baseClippingPlanes, boolean[] isBack) { + Vector4f[] planes = baseClippingPlanes.getPlanes(); + + for (int planeIndex = 0; planeIndex < planes.length; planeIndex++) { + if (!isBack[planeIndex]) { + continue; + } + + Vector4f plane = planes[planeIndex]; + + NeighboringPlaneSet neighbors = NeighboringPlaneSet.forPlane(planeIndex); + + if (!isBack[neighbors.getPlane0()]) { + addEdgePlane(plane, planes[neighbors.getPlane0()]); + } + + if (!isBack[neighbors.getPlane1()]) { + addEdgePlane(plane, planes[neighbors.getPlane1()]); + } + + if (!isBack[neighbors.getPlane2()]) { + addEdgePlane(plane, planes[neighbors.getPlane2()]); + } + + if (!isBack[neighbors.getPlane3()]) { + addEdgePlane(plane, planes[neighbors.getPlane3()]); + } + } + } + + private Vector3f truncate(Vector4f base) { + return new Vector3f(base.x(), base.y(), base.z()); + } + + private Vector4f extend(Vector3f base, float w) { + return new Vector4f(base.x(), base.y(), base.z(), w); + } + + private float lengthSquared(Vector3f v) { + float x = v.x(); + float y = v.y(); + float z = v.z(); + + return x * x + y * y + z * z; + } + + private Vector3f cross(Vector3f first, Vector3f second) { + Vector3f result = new Vector3f(first.x(), first.y(), first.z()); + result.cross(second); + + return result; + } + + private void addEdgePlane(Vector4f backPlane4, Vector4f frontPlane4) { + Vector3f backPlaneNormal = truncate(backPlane4); + Vector3f frontPlaneNormal = truncate(frontPlane4); + + // vector along the intersection of the two planes + Vector3f intersection = cross(backPlaneNormal, frontPlaneNormal); + + // compute edge plane normal, we want the normal vector of the edge plane + // to always be perpendicular to the shadow light vector (since that's + // what makes it an edge plane!) + Vector3f edgePlaneNormal = cross(intersection, shadowLightVectorFromOrigin); + + // At this point, we have a normal vector for our new edge plane, but we don't + // have a value for distance (d). We can solve for it with a little algebra, + // given that we want all 3 planes to intersect at a line. + + // Given the following system of equations: + // a₁x + b₁y + c₁z = d₁ + // a₂x + b₂y + c₂z = d₂ + // a₃x + b₃y + c₃z = d₃ + // + // Solve for -d₃, if a₁, b₁, c₁, -d₁, a₂, b₂, c₂, -d₂, a₃, b₃, and c₃ are all known, such that + // the 3 planes formed by the corresponding 3 plane equations intersect at a line. + + // First, we need to pick a point along the intersection line between our planes. + // Unfortunately, we don't have a complete line - only its vector. + // + // Fortunately, we can compute that point. If we create a plane passing through the origin + // with a normal vector parallel to the intersection line, then the intersection + // of all 3 planes will be a point on the line of intersection between the two planes we care about. + Vector3f point; + + { + // "Line of intersection between two planes" + // https://stackoverflow.com/a/32410473 by ideasman42, CC BY-SA 3.0 + // (a modified version of "Intersection of 2-planes" from Graphics Gems 1, page 305 + + // NB: We can assume that the intersection vector has a non-zero length. + Vector3f ixb = cross(intersection, backPlaneNormal); + Vector3f fxi = cross(frontPlaneNormal, intersection); + + ixb.mul(-frontPlane4.w()); + fxi.mul(-backPlane4.w()); + + ixb.add(fxi); + + point = ixb; + point.mul(1.0F / lengthSquared(intersection)); + } + + // Now that we have a point and a normal vector, we can make a plane. + + Vector4f plane; + + { + // dot(normal, (x, y, z) - point) = 0 + // a(x - point.x) + b(y - point.y) + c(z - point.z) = 0 + // d = a * point.x + b * point.y + c * point.z = dot(normal, point) + // w = -d + + float d = edgePlaneNormal.dot(point); + float w = -d; + + plane = extend(edgePlaneNormal, w); + } + + // Check and make sure our point is actually on all 3 planes. + // This can be removed in production but it's good to check for now while we're still testing. + /*{ + float dp0 = plane.dotProduct(extend(point, 1.0F)); + float dp1 = frontPlane4.dotProduct(extend(point, 1.0F)); + float dp2 = backPlane4.dotProduct(extend(point, 1.0F)); + + if (Math.abs(dp0) > 0.0005) { + throw new IllegalStateException("dp0 should be zero, but was " + dp0); + } + + if (Math.abs(dp1) > 0.0005) { + throw new IllegalStateException("dp1 should be zero, but was " + dp1); + } + + if (Math.abs(dp2) > 0.0005) { + throw new IllegalStateException("dp2 should be zero, but was " + dp2); + } + }*/ + + addPlane(plane); + } + + // Note: These functions are copied & modified from the vanilla Frustum class. + @Override + public void setPosition(double cameraX, double cameraY, double cameraZ) { + if (this.boxCuller != null) { + boxCuller.setPosition(cameraX, cameraY, cameraZ); + } + + this.x = cameraX; + this.y = cameraY; + this.z = cameraZ; + } + + @Override + public boolean isBoundingBoxInFrustum(AxisAlignedBB aabb) { + if (boxCuller != null && boxCuller.isCulled(aabb)) { + return false; + } + + return this.isVisible(aabb.minX, aabb.minY, aabb.minZ, aabb.maxX, aabb.maxY, aabb.maxZ); + } + + // For Sodium + // TODO: change this to respect intersections on 1.18+! + public boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + if (boxCuller != null && boxCuller.isCulled(minX, minY, minZ, maxX, maxY, maxZ)) { + return false; + } + + return isVisible(minX, minY, minZ, maxX, maxY, maxZ); + } + + // For Immersive Portals + // TODO: Figure out if IP culling can somehow be compatible with Iris culling. + public boolean canDetermineInvisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return false; + } + + private boolean isVisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + float f = (float)(minX - this.x); + float g = (float)(minY - this.y); + float h = (float)(minZ - this.z); + float i = (float)(maxX - this.x); + float j = (float)(maxY - this.y); + float k = (float)(maxZ - this.z); + return this.checkCornerVisibility(f, g, h, i, j, k) != 0; + } + + + /** + * Checks corner visibility. + * @param minX Minimum X value of the AABB. + * @param minY Minimum Y value of the AABB. + * @param minZ Minimum Z value of the AABB. + * @param maxX Maximum X value of the AABB. + * @param maxY Maximum Y value of the AABB. + * @param maxZ Maximum Z value of the AABB. + * @return 0 if nothing is visible, 1 if everything is visible, 2 if only some corners are visible. + */ + private int checkCornerVisibility(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + boolean inside = true; + float outsideBoundX; + float outsideBoundY; + float outsideBoundZ; + float insideBoundX; + float insideBoundY; + float insideBoundZ; + + for (int i = 0; i < planeCount; ++i) { + Vector4f plane = this.planes[i]; + + // Check if plane is inside or intersecting. + // This is ported from JOML's FrustumIntersection. + + if (plane.x() < 0) { + outsideBoundX = minX; + insideBoundX = maxX; + } else { + outsideBoundX = maxX; + insideBoundX = minX; + } + + if (plane.y() < 0) { + outsideBoundY = minY; + insideBoundY = maxY; + } else { + outsideBoundY = maxY; + insideBoundY = minY; + } + + if (plane.z() < 0) { + outsideBoundZ = minZ; + insideBoundZ = maxZ; + } else { + outsideBoundZ = maxZ; + insideBoundZ = minZ; + } + + if (Math.fma(plane.x(), outsideBoundX, Math.fma(plane.y(), outsideBoundY, plane.z() * outsideBoundZ)) < -plane.w()) { + return 0; + } + inside &= Math.fma(plane.x(), insideBoundX, Math.fma(plane.y(), insideBoundY, plane.z() * insideBoundZ)) >= -plane.w(); + } + + return 2; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/advanced/BaseClippingPlanes.java b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/BaseClippingPlanes.java new file mode 100644 index 000000000..c4d155350 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/BaseClippingPlanes.java @@ -0,0 +1,41 @@ +package net.coderbot.iris.shadows.frustum.advanced; + +import org.joml.Matrix4f; +import org.joml.Vector4f; + +public class BaseClippingPlanes { + private final Vector4f[] planes = new Vector4f[6]; + + public BaseClippingPlanes(Matrix4f playerView, Matrix4f playerProjection) { + this.init(playerView, playerProjection); + } + + private void init(Matrix4f view, Matrix4f projection) { + // Transform = Transpose(Projection x View) + + Matrix4f transform = new Matrix4f(projection); + transform.mul(view); + transform.transpose(); + + planes[0] = transform(transform, -1, 0, 0); + planes[1] = transform(transform, 1, 0, 0); + planes[2] = transform(transform, 0, -1, 0); + planes[3] = transform(transform, 0, 1, 0); + // FAR clipping plane + planes[4] = transform(transform, 0, 0, -1); + // NEAR clipping plane + planes[5] = transform(transform, 0, 0, 1); + } + + private static Vector4f transform(Matrix4f transform, float x, float y, float z) { + Vector4f vector4f = new Vector4f(x, y, z, 1.0F); + vector4f.mul(transform); + vector4f.normalize(); + + return vector4f; + } + + public Vector4f[] getPlanes() { + return planes; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/advanced/NeighboringPlaneSet.java b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/NeighboringPlaneSet.java new file mode 100644 index 000000000..d631f6bc6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/advanced/NeighboringPlaneSet.java @@ -0,0 +1,45 @@ +package net.coderbot.iris.shadows.frustum.advanced; + +public class NeighboringPlaneSet { + private static final NeighboringPlaneSet FOR_PLUS_X = new NeighboringPlaneSet(2, 3, 4, 5); + private static final NeighboringPlaneSet FOR_PLUS_Y = new NeighboringPlaneSet(0, 1, 4, 5); + private static final NeighboringPlaneSet FOR_PLUS_Z = new NeighboringPlaneSet(0, 1, 2, 3); + + private static final NeighboringPlaneSet[] TABLE = new NeighboringPlaneSet[] { + FOR_PLUS_X, + FOR_PLUS_Y, + FOR_PLUS_Z + }; + + private final int plane0; + private final int plane1; + private final int plane2; + private final int plane3; + + public NeighboringPlaneSet(int plane0, int plane1, int plane2, int plane3) { + this.plane0 = plane0; + this.plane1 = plane1; + this.plane2 = plane2; + this.plane3 = plane3; + } + + public static NeighboringPlaneSet forPlane(int planeIndex) { + return TABLE[planeIndex >>> 1]; + } + + public int getPlane0() { + return plane0; + } + + public int getPlane1() { + return plane1; + } + + public int getPlane2() { + return plane2; + } + + public int getPlane3() { + return plane3; + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/fallback/BoxCullingFrustum.java b/src/main/java/net/coderbot/iris/shadows/frustum/fallback/BoxCullingFrustum.java new file mode 100644 index 000000000..233dfece7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/fallback/BoxCullingFrustum.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.shadows.frustum.fallback; + +import net.coderbot.iris.shadows.frustum.BoxCuller; +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.util.AxisAlignedBB; + +public class BoxCullingFrustum extends Frustrum { + private final BoxCuller boxCuller; + + public BoxCullingFrustum(BoxCuller boxCuller) { + this.boxCuller = boxCuller; + } + + @Override + public void setPosition(double cameraX, double cameraY, double cameraZ) { + boxCuller.setPosition(cameraX, cameraY, cameraZ); + } + + // for Sodium + // TODO: Better way to do this... Maybe we shouldn't be using a frustum for the box culling in the first place! + public boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return !boxCuller.isCulled(minX, minY, minZ, maxX, maxY, maxZ); + } + + // For Immersive Portals + // NB: The shadow culling in Immersive Portals must be disabled, because when Advanced Shadow Frustum Culling + // is not active, we are at a point where we can make no assumptions how the shader pack uses the shadow + // pass beyond what it already tells us. So we cannot use any extra fancy culling methods. + public boolean canDetermineInvisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return false; + } + + @Override + public boolean isBoundingBoxInFrustum(AxisAlignedBB aabb) { + return !boxCuller.isCulled(aabb); + } +} diff --git a/src/main/java/net/coderbot/iris/shadows/frustum/fallback/NonCullingFrustum.java b/src/main/java/net/coderbot/iris/shadows/frustum/fallback/NonCullingFrustum.java new file mode 100644 index 000000000..284debfa6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shadows/frustum/fallback/NonCullingFrustum.java @@ -0,0 +1,26 @@ +package net.coderbot.iris.shadows.frustum.fallback; + +import net.minecraft.client.renderer.culling.Frustrum; +import net.minecraft.util.AxisAlignedBB; + + +public class NonCullingFrustum extends Frustrum { + + // for Sodium + public boolean fastAabbTest(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return true; + } + + // For Immersive Portals + // NB: The shadow culling in Immersive Portals must be disabled, because when Advanced Shadow Frustum Culling + // is not active, we are at a point where we can make no assumptions how the shader pack uses the shadow + // pass beyond what it already tells us. So we cannot use any extra fancy culling methods. + public boolean canDetermineInvisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return false; + } + + @Override + public boolean isBoundingBoxInFrustum(AxisAlignedBB aabb) { + return true; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/IrisChunkShaderBindingPoints.java b/src/main/java/net/coderbot/iris/sodium/IrisChunkShaderBindingPoints.java new file mode 100644 index 000000000..ad470fbfe --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/IrisChunkShaderBindingPoints.java @@ -0,0 +1,16 @@ +package net.coderbot.iris.sodium; + +import me.jellysquid.mods.sodium.client.gl.shader.ShaderBindingPoint; + +/** + * Defines Iris-specific chunk shader binding points. + * + * NB: Make sure this doesn't collide with anything in {@link me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints} + */ +public class IrisChunkShaderBindingPoints { + public static final ShaderBindingPoint NORMAL = new ShaderBindingPoint(5); + public static final ShaderBindingPoint TANGENT = new ShaderBindingPoint(6); + public static final ShaderBindingPoint MID_TEX_COORD = new ShaderBindingPoint(7); + public static final ShaderBindingPoint BLOCK_ID = new ShaderBindingPoint(8); + public static final ShaderBindingPoint MID_BLOCK = new ShaderBindingPoint(9); +} diff --git a/src/main/java/net/coderbot/iris/sodium/Mixins.txt b/src/main/java/net/coderbot/iris/sodium/Mixins.txt new file mode 100644 index 000000000..0dc133c82 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/Mixins.txt @@ -0,0 +1,39 @@ +Y directional_shading + Y MixinSmoothLightPipeline.java + Y MixinFlatLightPipeline.java +* shader_overrides + * ShaderTypeAccessor.java + Y MixinChunkRenderShaderBackend.java + Y MixinShaderType.java + Y MixinChunkProgram.java + Y MixinChunkRenderManager.java +* shadow_map + Y MixinSodiumWorldRenderer.java + Y MixinGameRendererContext.java + Y MixinChunkRenderManager.java +* block_id + Y MixinChunkRenderRebuildTask.java + Y MixinChunkBuildBuffers.java +* pbr_animation + * MixinTextureAtlasSprite.java +* vertex_format + * entity + * MixinBufferBuilder_ExtendedVertexFormatCompat.java // Unclear + Y MixinSodiumWorldRenderer.java + Y MixinChunkOneshotGraphicsState.java + N GlVertexAttributeFormatAccessor.java + Y MixinMultidrawChunkRenderBackend.java + Y MixinChunkMeshAttribute.java + Y MixinGlVertexFormatBuilder.java + N ChunkMeshAttributeAccessor.java +* separate_ao + * MixinBlockRenderer.java + * MixinFluidRenderer.java + * MixinBufferBuilder_IntrinsicSeparateAo.java +* options + X MixinSodiumGameOptionPages.java + X MixinOptionImpl.java + X MixinSodiumGameOptions.java + X MixinSodiumOptionsGUI.java + X MixinChunkRenderManager.java +* IrisSodiumCompatMixinPlugin.java diff --git a/src/main/java/net/coderbot/iris/sodium/block_context/BlockContextHolder.java b/src/main/java/net/coderbot/iris/sodium/block_context/BlockContextHolder.java new file mode 100644 index 000000000..6f7e601ff --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/block_context/BlockContextHolder.java @@ -0,0 +1,47 @@ +package net.coderbot.iris.sodium.block_context; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import net.minecraft.block.Block; + +public class BlockContextHolder { + private final Object2IntMap blockMatches; + + public int localPosX; + public int localPosY; + public int localPosZ; + + public short blockId; + public short renderType; + + public BlockContextHolder() { + this.blockMatches = Object2IntMaps.emptyMap(); + this.blockId = -1; + this.renderType = -1; + } + + public BlockContextHolder(Object2IntMap idMap) { + this.blockMatches = idMap; + this.blockId = -1; + this.renderType = -1; + } + + public void setLocalPos(int localPosX, int localPosY, int localPosZ) { + this.localPosX = localPosX; + this.localPosY = localPosY; + this.localPosZ = localPosZ; + } + + public void set(Block block, short renderType) { + this.blockId = (short) this.blockMatches.getOrDefault(block, -1); + this.renderType = renderType; + } + + public void reset() { + this.blockId = -1; + this.renderType = -1; + this.localPosX = 0; + this.localPosY = 0; + this.localPosZ = 0; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/block_context/ChunkBuildBuffersExt.java b/src/main/java/net/coderbot/iris/sodium/block_context/ChunkBuildBuffersExt.java new file mode 100644 index 000000000..407b3cde7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/block_context/ChunkBuildBuffersExt.java @@ -0,0 +1,11 @@ +package net.coderbot.iris.sodium.block_context; + +import net.minecraft.block.Block; + +public interface ChunkBuildBuffersExt { + void iris$setLocalPos(int localPosX, int localPosY, int localPosZ); + + void iris$setMaterialId(Block block, short renderType); + + void iris$resetBlockContext(); +} diff --git a/src/main/java/net/coderbot/iris/sodium/block_context/ContextAwareVertexWriter.java b/src/main/java/net/coderbot/iris/sodium/block_context/ContextAwareVertexWriter.java new file mode 100644 index 000000000..b0ba32679 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/block_context/ContextAwareVertexWriter.java @@ -0,0 +1,5 @@ +package net.coderbot.iris.sodium.block_context; + +public interface ContextAwareVertexWriter { + void iris$setContextHolder(BlockContextHolder holder); +} diff --git a/src/main/java/net/coderbot/iris/sodium/shader_overrides/ChunkRenderBackendExt.java b/src/main/java/net/coderbot/iris/sodium/shader_overrides/ChunkRenderBackendExt.java new file mode 100644 index 000000000..311632a70 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shader_overrides/ChunkRenderBackendExt.java @@ -0,0 +1,8 @@ +package net.coderbot.iris.sodium.shader_overrides; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; + +public interface ChunkRenderBackendExt { + void iris$begin(MatrixStack matrixStack, BlockRenderPass pass); +} diff --git a/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgram.java b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgram.java new file mode 100644 index 000000000..240927f46 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgram.java @@ -0,0 +1,96 @@ +package net.coderbot.iris.sodium.shader_overrides; + +import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkProgram; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderFogComponent; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.program.ProgramImages; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; + +public class IrisChunkProgram extends ChunkProgram { + // Uniform variable binding indexes + private final int uModelViewMatrix; + private final int uNormalMatrix; + + @Nullable + private final ProgramUniforms irisProgramUniforms; + + @Nullable + private final ProgramSamplers irisProgramSamplers; + + @Nullable + private final ProgramImages irisProgramImages; + + public IrisChunkProgram(RenderDevice owner, ResourceLocation name, int handle, + @Nullable ProgramUniforms irisProgramUniforms, @Nullable ProgramSamplers irisProgramSamplers, + @Nullable ProgramImages irisProgramImages) { + super(owner, name, handle, ChunkShaderFogComponent.None::new); + this.uModelViewMatrix = this.getUniformLocation("iris_ModelViewMatrix"); + this.uNormalMatrix = this.getUniformLocation("iris_NormalMatrix"); + this.irisProgramUniforms = irisProgramUniforms; + this.irisProgramSamplers = irisProgramSamplers; + this.irisProgramImages = irisProgramImages; + } + + public void setup(MatrixStack matrixStack, float modelScale, float textureScale) { + super.setup(matrixStack, modelScale, textureScale); + + if (irisProgramUniforms != null) { + irisProgramUniforms.update(); + } + + if (irisProgramSamplers != null) { + irisProgramSamplers.update(); + } + + if (irisProgramImages != null) { + irisProgramImages.update(); + } + + Matrix4f modelViewMatrix = matrixStack.peek().getModel(); + Matrix4f normalMatrix = new Matrix4f(matrixStack.peek().getModel()); + normalMatrix.invert(); + normalMatrix.transpose(); + + uniformMatrix(uModelViewMatrix, modelViewMatrix); + uniformMatrix(uNormalMatrix, normalMatrix); + } + + @Override + public int getUniformLocation(String name) { + // NB: We pass through calls involving u_ModelViewProjectionMatrix, u_ModelScale, and u_TextureScale, since + // currently patched Iris shader programs use those. + + if ("iris_BlockTex".equals(name) || "iris_LightTex".equals(name)) { + // Not relevant for Iris shader programs + return -1; + } + + try { + return super.getUniformLocation(name); + } catch (NullPointerException e) { + // Suppress getUniformLocation + return -1; + } + } + + private void uniformMatrix(int location, Matrix4f matrix) { + if (location == -1) { + return; + } + + FloatBuffer buffer = BufferUtils.createFloatBuffer(16); + + matrix.get(buffer); + + IrisRenderSystem.uniformMatrix4fv(location, false, buffer); + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgramOverrides.java b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgramOverrides.java new file mode 100644 index 000000000..97b26ff4c --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisChunkProgramOverrides.java @@ -0,0 +1,219 @@ +package net.coderbot.iris.sodium.shader_overrides; + +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.gl.shader.GlProgram; +import me.jellysquid.mods.sodium.client.gl.shader.GlShader; +import me.jellysquid.mods.sodium.client.gl.shader.ShaderConstants; +import me.jellysquid.mods.sodium.client.gl.shader.ShaderType; +import me.jellysquid.mods.sodium.client.render.chunk.passes.BlockRenderPass; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkProgram; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.program.ProgramImages; +import net.coderbot.iris.gl.program.ProgramSamplers; +import net.coderbot.iris.gl.program.ProgramUniforms; +import net.coderbot.iris.pipeline.SodiumTerrainPipeline; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.coderbot.iris.shadows.ShadowRenderingState; +import net.coderbot.iris.sodium.IrisChunkShaderBindingPoints; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumMap; +import java.util.Locale; +import java.util.Optional; + +public class IrisChunkProgramOverrides { + private static final ShaderConstants EMPTY_CONSTANTS = ShaderConstants.builder().build(); + + private final EnumMap programs = new EnumMap<>(IrisTerrainPass.class); + + private int versionCounterForSodiumShaderReload = -1; + + private GlShader createVertexShader(RenderDevice device, IrisTerrainPass pass, SodiumTerrainPipeline pipeline) { + Optional irisVertexShader; + + if (pass == IrisTerrainPass.SHADOW) { + irisVertexShader = pipeline.getShadowVertexShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_SOLID) { + irisVertexShader = pipeline.getTerrainVertexShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_TRANSLUCENT) { + irisVertexShader = pipeline.getTranslucentVertexShaderSource(); + } else { + throw new IllegalArgumentException("Unknown pass type " + pass); + } + + String source = irisVertexShader.orElse(null); + + if (source == null) { + return null; + } + + return new GlShader(device, ShaderType.VERTEX, new ResourceLocation("iris", + "sodium-terrain-" + pass.toString().toLowerCase(Locale.ROOT) + ".vsh"), source, EMPTY_CONSTANTS); + } + + private GlShader createGeometryShader(RenderDevice device, IrisTerrainPass pass, SodiumTerrainPipeline pipeline) { + Optional irisGeometryShader; + + if (pass == IrisTerrainPass.SHADOW) { + irisGeometryShader = pipeline.getShadowGeometryShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_SOLID) { + irisGeometryShader = pipeline.getTerrainGeometryShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_TRANSLUCENT) { + irisGeometryShader = pipeline.getTranslucentGeometryShaderSource(); + } else { + throw new IllegalArgumentException("Unknown pass type " + pass); + } + + String source = irisGeometryShader.orElse(null); + + if (source == null) { + return null; + } + + return new GlShader(device, IrisShaderTypes.GEOMETRY, new ResourceLocation("iris", + "sodium-terrain-" + pass.toString().toLowerCase(Locale.ROOT) + ".gsh"), source, EMPTY_CONSTANTS); + } + + private GlShader createFragmentShader(RenderDevice device, IrisTerrainPass pass, SodiumTerrainPipeline pipeline) { + Optional irisFragmentShader; + + if (pass == IrisTerrainPass.SHADOW) { + irisFragmentShader = pipeline.getShadowFragmentShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_SOLID) { + irisFragmentShader = pipeline.getTerrainFragmentShaderSource(); + } else if (pass == IrisTerrainPass.GBUFFER_TRANSLUCENT) { + irisFragmentShader = pipeline.getTranslucentFragmentShaderSource(); + } else { + throw new IllegalArgumentException("Unknown pass type " + pass); + } + + String source = irisFragmentShader.orElse(null); + + if (source == null) { + return null; + } + + return new GlShader(device, ShaderType.FRAGMENT, new ResourceLocation("iris", + "sodium-terrain-" + pass.toString().toLowerCase(Locale.ROOT) + ".fsh"), source, EMPTY_CONSTANTS); + } + + @Nullable + private ChunkProgram createShader(RenderDevice device, IrisTerrainPass pass, SodiumTerrainPipeline pipeline) { + GlShader vertShader = createVertexShader(device, pass, pipeline); + GlShader geomShader = createGeometryShader(device, pass, pipeline); + GlShader fragShader = createFragmentShader(device, pass, pipeline); + + if (vertShader == null || fragShader == null) { + if (vertShader != null) { + vertShader.delete(); + } + + if (geomShader != null) { + geomShader.delete(); + } + + if (fragShader != null) { + fragShader.delete(); + } + + // TODO: Partial shader programs? + return null; + } + + try { + GlProgram.Builder builder = GlProgram.builder(new ResourceLocation("sodium", "chunk_shader_for_" + + pass.getName())); + + if (geomShader != null) { + builder.attachShader(geomShader); + } + + return builder.attachShader(vertShader) + .attachShader(fragShader) + .bindAttribute("iris_Pos", ChunkShaderBindingPoints.POSITION) + .bindAttribute("iris_Color", ChunkShaderBindingPoints.COLOR) + .bindAttribute("iris_TexCoord", ChunkShaderBindingPoints.TEX_COORD) + .bindAttribute("iris_LightCoord", ChunkShaderBindingPoints.LIGHT_COORD) + .bindAttribute("iris_Normal", IrisChunkShaderBindingPoints.NORMAL) + .bindAttribute("at_tangent", IrisChunkShaderBindingPoints.TANGENT) + .bindAttribute("mc_midTexCoord", IrisChunkShaderBindingPoints.MID_TEX_COORD) + .bindAttribute("mc_Entity", IrisChunkShaderBindingPoints.BLOCK_ID) + .bindAttribute("at_midBlock", IrisChunkShaderBindingPoints.MID_BLOCK) + .bindAttribute("iris_ModelOffset", ChunkShaderBindingPoints.MODEL_OFFSET) + .build((program, name) -> { + ProgramUniforms uniforms = pipeline.initUniforms(name); + ProgramSamplers samplers; + ProgramImages images; + + if (pass == IrisTerrainPass.SHADOW) { + samplers = pipeline.initShadowSamplers(name); + images = pipeline.initShadowImages(name); + } else { + samplers = pipeline.initTerrainSamplers(name); + images = pipeline.initTerrainImages(name); + } + + return new IrisChunkProgram(device, program, name, uniforms, samplers, images); + }); + } finally { + vertShader.delete(); + if (geomShader != null) { + geomShader.delete(); + } + fragShader.delete(); + } + } + + public void createShaders(SodiumTerrainPipeline sodiumTerrainPipeline, RenderDevice device) { + if (sodiumTerrainPipeline != null) { + for (IrisTerrainPass pass : IrisTerrainPass.values()) { + if (pass == IrisTerrainPass.SHADOW && !sodiumTerrainPipeline.hasShadowPass()) { + this.programs.put(pass, null); + continue; + } + + this.programs.put(pass, createShader(device, pass, sodiumTerrainPipeline)); + } + } else { + this.programs.clear(); + } + } + + @Nullable + public ChunkProgram getProgramOverride(RenderDevice device, BlockRenderPass pass) { + WorldRenderingPipeline worldRenderingPipeline = Iris.getPipelineManager().getPipelineNullable(); + SodiumTerrainPipeline sodiumTerrainPipeline = null; + + if (worldRenderingPipeline != null) { + sodiumTerrainPipeline = worldRenderingPipeline.getSodiumTerrainPipeline(); + } + + if (versionCounterForSodiumShaderReload != Iris.getPipelineManager().getVersionCounterForSodiumShaderReload()) { + versionCounterForSodiumShaderReload = Iris.getPipelineManager().getVersionCounterForSodiumShaderReload(); + deleteShaders(); + createShaders(sodiumTerrainPipeline, device); + } + + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + if (sodiumTerrainPipeline != null && !sodiumTerrainPipeline.hasShadowPass()) { + throw new IllegalStateException("Shadow program requested, but the pack does not have a shadow pass?"); + } + + return this.programs.get(IrisTerrainPass.SHADOW); + } else { + return this.programs.get(pass.isTranslucent() ? IrisTerrainPass.GBUFFER_TRANSLUCENT : IrisTerrainPass.GBUFFER_SOLID); + } + } + + public void deleteShaders() { + for (ChunkProgram program : this.programs.values()) { + if (program != null) { + program.delete(); + } + } + + this.programs.clear(); + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisShaderTypes.java b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisShaderTypes.java new file mode 100644 index 000000000..f1a7ba002 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisShaderTypes.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.sodium.shader_overrides; + +import me.jellysquid.mods.sodium.client.gl.shader.ShaderType; + +/** + * Initialized by {@link net.coderbot.iris.compat.sodium.mixin.shader_overrides.MixinShaderType} + */ +public class IrisShaderTypes { + public static ShaderType GEOMETRY; +} diff --git a/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisTerrainPass.java b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisTerrainPass.java new file mode 100644 index 000000000..1c0d6a2a8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shader_overrides/IrisTerrainPass.java @@ -0,0 +1,17 @@ +package net.coderbot.iris.sodium.shader_overrides; + +public enum IrisTerrainPass { + SHADOW("shadow"), + GBUFFER_SOLID("gbuffers_terrain"), + GBUFFER_TRANSLUCENT("gbuffers_water"); + + private final String name; + + IrisTerrainPass(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/shadow_map/SwappableChunkRenderManager.java b/src/main/java/net/coderbot/iris/sodium/shadow_map/SwappableChunkRenderManager.java new file mode 100644 index 000000000..1692cf79c --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/shadow_map/SwappableChunkRenderManager.java @@ -0,0 +1,5 @@ +package net.coderbot.iris.sodium.shadow_map; + +public interface SwappableChunkRenderManager { + void iris$swapVisibilityState(); +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisGlVertexAttributeFormat.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisGlVertexAttributeFormat.java new file mode 100644 index 000000000..b2736642c --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisGlVertexAttributeFormat.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.sodium.vertex_format; + +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import org.lwjgl.opengl.GL11; + +public class IrisGlVertexAttributeFormat { + public static final GlVertexAttributeFormat BYTE = new GlVertexAttributeFormat(GL11.GL_BYTE, 1); + public static final GlVertexAttributeFormat SHORT = new GlVertexAttributeFormat(GL11.GL_SHORT, 2); +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisModelVertexFormats.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisModelVertexFormats.java new file mode 100644 index 000000000..b8c963997 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/IrisModelVertexFormats.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.sodium.vertex_format; + +import net.coderbot.iris.sodium.vertex_format.terrain_xhfp.XHFPModelVertexType; + +public class IrisModelVertexFormats { + public static final XHFPModelVertexType MODEL_VERTEX_XHFP = new XHFPModelVertexType(); +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterNio.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterNio.java new file mode 100644 index 000000000..a649ff0be --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterNio.java @@ -0,0 +1,96 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.toremove.OverlayTexture; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import net.coderbot.iris.vertices.IrisVertexFormats; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +import java.nio.ByteBuffer; + +public class EntityVertexBufferWriterNio extends VertexBufferWriterNio implements QuadVertexSink, GlyphVertexSink { + private static final int STRIDE = IrisVertexFormats.ENTITY.getVertexSize(); + + private final QuadViewEntity.QuadViewEntityNio quad = new QuadViewEntity.QuadViewEntityNio(); + private final Vector3f saveNormal = new Vector3f(); + + private int vertexCount; + private float uSum; + private float vSum; + + public EntityVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, ExtendedQuadVertexType.INSTANCE); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + int i = this.writeOffset; + ByteBuffer buffer = this.byteBuffer; + + vertexCount++; + uSum += u; + vSum += v; + + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putInt(i + 12, color); + buffer.putFloat(i + 16, u); + buffer.putFloat(i + 20, v); + buffer.putInt(i + 24, overlay); + buffer.putInt(i + 28, light); + + if (vertexCount == 4) { + this.endQuad(normal); + } + + this.advance(); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + writeQuad(x, y, z, color, u, v, light, OverlayTexture.NO_OVERLAY, 0); + } + + private void endQuad(int normal) { + this.vertexCount = 0; + + int i = this.writeOffset; + ByteBuffer buffer = this.byteBuffer; + + uSum *= 0.25; + vSum *= 0.25; + + quad.setup(buffer, i, STRIDE); + + float normalX, normalY, normalZ; + + if (normal == 0) { + NormalHelper.computeFaceNormal(saveNormal, quad); + normalX = saveNormal.x; + normalY = saveNormal.y; + normalZ = saveNormal.z; + normal = NormalHelper.packNormal(saveNormal, 0.0F); + } else { + normalX = Norm3b.unpackX(normal); + normalY = Norm3b.unpackY(normal); + normalZ = Norm3b.unpackZ(normal); + } + + int tangent = NormalHelper.computeTangent(normalX, normalY, normalZ, quad); + + for (int vertex = 0; vertex < 4; vertex++) { + buffer.putFloat(i + 36 - STRIDE * vertex, uSum); + buffer.putFloat(i + 40 - STRIDE * vertex, vSum); + buffer.putInt(i + 32 - STRIDE * vertex, normal); + buffer.putInt(i + 44 - STRIDE * vertex, tangent); + } + + uSum = 0; + vSum = 0; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterUnsafe.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..bbb0e3913 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/EntityVertexBufferWriterUnsafe.java @@ -0,0 +1,93 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import com.gtnewhorizons.angelica.compat.toremove.OverlayTexture; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import net.coderbot.iris.vertices.IrisVertexFormats; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +public class EntityVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements QuadVertexSink, GlyphVertexSink { + private static final int STRIDE = IrisVertexFormats.ENTITY.getVertexSize(); + + private final QuadViewEntity.QuadViewEntityUnsafe quad = new QuadViewEntity.QuadViewEntityUnsafe(); + private final Vector3f saveNormal = new Vector3f(); + + private int vertexCount; + private float uSum; + private float vSum; + + public EntityVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, ExtendedQuadVertexType.INSTANCE); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + long i = this.writePointer; + + vertexCount++; + uSum += u; + vSum += v; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + CompatMemoryUtil.memPutFloat(i + 16, u); + CompatMemoryUtil.memPutFloat(i + 20, v); + CompatMemoryUtil.memPutInt(i + 24, overlay); + CompatMemoryUtil.memPutInt(i + 28, light); + + if (vertexCount == 4) { + this.endQuad(normal); + } + + this.advance(); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + writeQuad(x, y, z, color, u, v, light, OverlayTexture.NO_OVERLAY, 0); + } + + private void endQuad(int normal) { + this.vertexCount = 0; + + long i = this.writePointer; + + uSum *= 0.25; + vSum *= 0.25; + + quad.setup(writePointer, STRIDE); + + float normalX, normalY, normalZ; + + if (normal == 0) { + NormalHelper.computeFaceNormal(saveNormal, quad); + normalX = saveNormal.x; + normalY = saveNormal.y; + normalZ = saveNormal.z; + normal = NormalHelper.packNormal(saveNormal, 0.0F); + } else { + normalX = Norm3b.unpackX(normal); + normalY = Norm3b.unpackY(normal); + normalZ = Norm3b.unpackZ(normal); + } + + int tangent = NormalHelper.computeTangent(normalX, normalY, normalZ, quad); + + for (long vertex = 0; vertex < 4; vertex++) { + CompatMemoryUtil.memPutFloat(i + 36 - STRIDE * vertex, uSum); + CompatMemoryUtil.memPutFloat(i + 40 - STRIDE * vertex, vSum); + CompatMemoryUtil.memPutInt(i + 32 - STRIDE * vertex, normal); + CompatMemoryUtil.memPutInt(i + 44 - STRIDE * vertex, tangent); + } + + uSum = 0; + vSum = 0; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedGlyphVertexType.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedGlyphVertexType.java new file mode 100644 index 000000000..2961eb041 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedGlyphVertexType.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.writer.GlyphVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; +import net.coderbot.iris.vertices.IrisVertexFormats; + +public class ExtendedGlyphVertexType implements VanillaVertexType, BlittableVertexType { + public static final ExtendedGlyphVertexType INSTANCE = new ExtendedGlyphVertexType(); + + @Override + public GlyphVertexSink createFallbackWriter(VertexConsumer vertexConsumer) { + return new GlyphVertexWriterFallback(vertexConsumer); + } + + @Override + public GlyphVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new GlyphVertexBufferWriterUnsafe(buffer) : new GlyphVertexBufferWriterNio(buffer); + } + + @Override + public VertexFormat getVertexFormat() { + return IrisVertexFormats.TERRAIN; + } + + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedQuadVertexType.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedQuadVertexType.java new file mode 100644 index 000000000..e2c128bd0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/ExtendedQuadVertexType.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.writer.QuadVertexWriterFallback; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.VanillaVertexType; +import net.coderbot.iris.vertices.IrisVertexFormats; + +public class ExtendedQuadVertexType implements VanillaVertexType, BlittableVertexType { + public static final ExtendedQuadVertexType INSTANCE = new ExtendedQuadVertexType(); + + @Override + public QuadVertexSink createFallbackWriter(VertexConsumer vertexConsumer) { + return new QuadVertexWriterFallback(vertexConsumer); + } + + @Override + public QuadVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new EntityVertexBufferWriterUnsafe(buffer) : new EntityVertexBufferWriterNio(buffer); + } + + @Override + public VertexFormat getVertexFormat() { + return IrisVertexFormats.ENTITY; + } + + public BlittableVertexType asBlittable() { + return this; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterNio.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterNio.java new file mode 100644 index 000000000..e2b0d4bce --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterNio.java @@ -0,0 +1,95 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.toremove.OverlayTexture; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import net.coderbot.iris.vertices.IrisVertexFormats; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +import java.nio.ByteBuffer; + +public class GlyphVertexBufferWriterNio extends VertexBufferWriterNio implements QuadVertexSink, GlyphVertexSink { + private static final int STRIDE = IrisVertexFormats.TERRAIN.getVertexSize(); + + private final QuadViewEntity.QuadViewEntityNio quad = new QuadViewEntity.QuadViewEntityNio(); + private final Vector3f saveNormal = new Vector3f(); + + private int vertexCount; + private float uSum; + private float vSum; + + public GlyphVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, ExtendedGlyphVertexType.INSTANCE); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + int i = this.writeOffset; + ByteBuffer buffer = this.byteBuffer; + + vertexCount++; + uSum += u; + vSum += v; + + buffer.putFloat(i, x); + buffer.putFloat(i + 4, y); + buffer.putFloat(i + 8, z); + buffer.putInt(i + 12, color); + buffer.putFloat(i + 16, u); + buffer.putFloat(i + 20, v); + buffer.putInt(i + 24, light); + + if (vertexCount == 4) { + this.endQuad(normal); + } + + this.advance(); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + writeQuad(x, y, z, color, u, v, light, OverlayTexture.NO_OVERLAY, 0); + } + + private void endQuad(int normal) { + this.vertexCount = 0; + + int i = this.writeOffset; + ByteBuffer buffer = this.byteBuffer; + + uSum *= 0.25; + vSum *= 0.25; + + quad.setup(byteBuffer, writeOffset, STRIDE); + + float normalX, normalY, normalZ; + + if (normal == 0) { + NormalHelper.computeFaceNormal(saveNormal, quad); + normalX = saveNormal.x; + normalY = saveNormal.y; + normalZ = saveNormal.z; + normal = NormalHelper.packNormal(saveNormal, 0.0F); + } else { + normalX = Norm3b.unpackX(normal); + normalY = Norm3b.unpackY(normal); + normalZ = Norm3b.unpackZ(normal); + } + + int tangent = NormalHelper.computeTangent(normalX, normalY, normalZ, quad); + + for (int vertex = 0; vertex < 4; vertex++) { + buffer.putFloat(i + 36 - STRIDE * vertex, uSum); + buffer.putFloat(i + 40 - STRIDE * vertex, vSum); + buffer.putInt(i + 28 - STRIDE * vertex, normal); + buffer.putInt(i + 44 - STRIDE * vertex, tangent); + } + + uSum = 0; + vSum = 0; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterUnsafe.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..66e087d61 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/GlyphVertexBufferWriterUnsafe.java @@ -0,0 +1,92 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import com.gtnewhorizons.angelica.compat.toremove.OverlayTexture; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.model.vertex.formats.glyph.GlyphVertexSink; +import me.jellysquid.mods.sodium.client.model.vertex.formats.quad.QuadVertexSink; +import me.jellysquid.mods.sodium.client.util.Norm3b; +import net.coderbot.iris.vertices.IrisVertexFormats; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +public class GlyphVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements QuadVertexSink, GlyphVertexSink { + private static final int STRIDE = IrisVertexFormats.TERRAIN.getVertexSize(); + + private final QuadViewEntity.QuadViewEntityUnsafe quad = new QuadViewEntity.QuadViewEntityUnsafe(); + private final Vector3f saveNormal = new Vector3f(); + + private int vertexCount; + private float uSum; + private float vSum; + + public GlyphVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, ExtendedGlyphVertexType.INSTANCE); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light, int overlay, int normal) { + long i = this.writePointer; + + vertexCount++; + uSum += u; + vSum += v; + + CompatMemoryUtil.memPutFloat(i, x); + CompatMemoryUtil.memPutFloat(i + 4, y); + CompatMemoryUtil.memPutFloat(i + 8, z); + CompatMemoryUtil.memPutInt(i + 12, color); + CompatMemoryUtil.memPutFloat(i + 16, u); + CompatMemoryUtil.memPutFloat(i + 20, v); + CompatMemoryUtil.memPutInt(i + 24, light); + + if (vertexCount == 4) { + this.endQuad(normal); + } + + this.advance(); + } + + @Override + public void writeGlyph(float x, float y, float z, int color, float u, float v, int light) { + writeQuad(x, y, z, color, u, v, light, OverlayTexture.NO_OVERLAY, 0); + } + + private void endQuad(int normal) { + this.vertexCount = 0; + + long i = this.writePointer; + + uSum *= 0.25; + vSum *= 0.25; + + quad.setup(writePointer, STRIDE); + + float normalX, normalY, normalZ; + + if (normal == 0) { + NormalHelper.computeFaceNormal(saveNormal, quad); + normalX = saveNormal.x; + normalY = saveNormal.y; + normalZ = saveNormal.z; + normal = NormalHelper.packNormal(saveNormal, 0.0F); + } else { + normalX = Norm3b.unpackX(normal); + normalY = Norm3b.unpackY(normal); + normalZ = Norm3b.unpackZ(normal); + } + + int tangent = NormalHelper.computeTangent(normalX, normalY, normalZ, quad); + + for (long vertex = 0; vertex < 4; vertex++) { + CompatMemoryUtil.memPutFloat(i + 36 - STRIDE * vertex, uSum); + CompatMemoryUtil.memPutFloat(i + 40 - STRIDE * vertex, vSum); + CompatMemoryUtil.memPutInt(i + 28 - STRIDE * vertex, normal); + CompatMemoryUtil.memPutInt(i + 44 - STRIDE * vertex, tangent); + } + + uSum = 0; + vSum = 0; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/QuadViewEntity.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/QuadViewEntity.java new file mode 100644 index 000000000..e8f59688f --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/entity_xhfp/QuadViewEntity.java @@ -0,0 +1,65 @@ +package net.coderbot.iris.sodium.vertex_format.entity_xhfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import net.coderbot.iris.vertices.QuadView; + +import java.nio.ByteBuffer; + +public abstract class QuadViewEntity implements QuadView { + long writePointer; + int stride; + + @Override + public float x(int index) { + return getFloat(writePointer - stride * (3L - index)); + } + + @Override + public float y(int index) { + return getFloat(writePointer + 4 - stride * (3L - index)); + } + + @Override + public float z(int index) { + return getFloat(writePointer + 8 - stride * (3L - index)); + } + + @Override + public float u(int index) { + return getFloat(writePointer + 16 - stride * (3L - index)); + } + + @Override + public float v(int index) { + return getFloat(writePointer + 20 - stride * (3L - index)); + } + + abstract float getFloat(long writePointer); + + public static class QuadViewEntityUnsafe extends QuadViewEntity { + public void setup(long writePointer, int stride) { + this.writePointer = writePointer; + this.stride = stride; + } + + @Override + float getFloat(long writePointer) { + return CompatMemoryUtil.memGetFloat(writePointer); + } + } + + public static class QuadViewEntityNio extends QuadViewEntity { + private ByteBuffer buffer; + + public void setup(ByteBuffer buffer, int writePointer, int stride) { + this.buffer = buffer; + this.writePointer = writePointer; + this.stride = stride; + } + + @Override + float getFloat(long writePointer) { + return buffer.getFloat((int) writePointer); + } + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/QuadViewTerrain.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/QuadViewTerrain.java new file mode 100644 index 000000000..b08298626 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/QuadViewTerrain.java @@ -0,0 +1,73 @@ +package net.coderbot.iris.sodium.vertex_format.terrain_xhfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import net.coderbot.iris.vertices.QuadView; + +import java.nio.ByteBuffer; + +public abstract class QuadViewTerrain implements QuadView { + long writePointer; + int stride; + + @Override + public float x(int index) { + return normalizeVertexPositionShortAsFloat(getShort(writePointer - stride * (3L - index))); + } + + @Override + public float y(int index) { + return normalizeVertexPositionShortAsFloat(getShort(writePointer + 2 - stride * (3L - index))); + } + + @Override + public float z(int index) { + return normalizeVertexPositionShortAsFloat(getShort(writePointer + 4 - stride * (3L - index))); + } + + @Override + public float u(int index) { + return normalizeVertexTextureShortAsFloat(getShort(writePointer + 12 - stride * (3L - index))); + } + + @Override + public float v(int index) { + return normalizeVertexTextureShortAsFloat(getShort(writePointer + 14 - stride * (3L - index))); + } + + private static float normalizeVertexTextureShortAsFloat(short value) { + return (value & 0xFFFF) * (1.0f / 32768.0f); + } + + private static float normalizeVertexPositionShortAsFloat(short value) { + return (value & 0xFFFF) * (1.0f / 65535.0f); + } + + abstract short getShort(long writePointer); + + public static class QuadViewTerrainUnsafe extends QuadViewTerrain { + public void setup(long writePointer, int stride) { + this.writePointer = writePointer; + this.stride = stride; + } + + @Override + short getShort(long writePointer) { + return CompatMemoryUtil.memGetShort(writePointer); + } + } + + public static class QuadViewTerrainNio extends QuadViewTerrain { + private ByteBuffer buffer; + + public void setup(ByteBuffer buffer, int writePointer, int stride) { + this.buffer = buffer; + this.writePointer = writePointer; + this.stride = stride; + } + + @Override + short getShort(long writePointer) { + return buffer.getShort((int) writePointer); + } + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterNio.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterNio.java new file mode 100644 index 000000000..0585aaab8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterNio.java @@ -0,0 +1,149 @@ +package net.coderbot.iris.sodium.vertex_format.terrain_xhfp; + +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterNio; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexUtil; +import net.coderbot.iris.sodium.block_context.BlockContextHolder; +import net.coderbot.iris.sodium.block_context.ContextAwareVertexWriter; +import net.coderbot.iris.sodium.vertex_format.IrisModelVertexFormats; +import net.coderbot.iris.vertices.ExtendedDataHelper; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +import java.nio.ByteBuffer; + +import static net.coderbot.iris.sodium.vertex_format.terrain_xhfp.XHFPModelVertexType.STRIDE; + +public class XHFPModelVertexBufferWriterNio extends VertexBufferWriterNio implements ModelVertexSink, ContextAwareVertexWriter { + private final QuadViewTerrain.QuadViewTerrainNio quad = new QuadViewTerrain.QuadViewTerrainNio(); + private final Vector3f normal = new Vector3f(); + + private BlockContextHolder contextHolder; + + private int vertexCount; + private float uSum; + private float vSum; + + public XHFPModelVertexBufferWriterNio(VertexBufferView backingBuffer) { + super(backingBuffer, IrisModelVertexFormats.MODEL_VERTEX_XHFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + uSum += u; + vSum += v; + + this.writeQuadInternal( + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(x), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(y), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(z), + color, + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(u), + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(v), + light, + contextHolder.blockId, + contextHolder.renderType, + ExtendedDataHelper.computeMidBlock(x, y, z, contextHolder.localPosX, contextHolder.localPosY, contextHolder.localPosZ) + ); + } + + private void writeQuadInternal(short x, short y, short z, int color, short u, short v, int light, short materialId, + short renderType, int packedMidBlock) { + int i = this.writeOffset; + + vertexCount++; + // NB: uSum and vSum must already be incremented outside of this function. + + ByteBuffer buffer = this.byteBuffer; + buffer.putShort(i, x); + buffer.putShort(i + 2, y); + buffer.putShort(i + 4, z); + buffer.putInt(i + 8, color); + buffer.putShort(i + 12, u); + buffer.putShort(i + 14, v); + buffer.putShort(i + 16, (short) (light & 0xFFFF)); + buffer.putShort(i + 18, (short) (light >> 16 & 0xFFFF)); + // NB: We don't set midTexCoord, normal, and tangent here, they will be filled in later. + // block ID: We only set the first 2 values, any legacy shaders using z or w will get filled in based on the GLSL spec + // https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_format + // TODO: can we pack this into one short? + buffer.putShort(i + 36, materialId); + buffer.putShort(i + 38, renderType); + buffer.putInt(i + 40, packedMidBlock); + + if (vertexCount == 4) { + vertexCount = 0; + + // FIXME + // The following logic is incorrect because OpenGL denormalizes shorts by dividing by 65535. The atlas is + // based on power-of-two values and so a normalization factor that is not a power of two causes the values + // used in the shader to be off by enough to cause visual errors. These are most noticeable on 1.18 with POM + // on block edges. + // + // The only reliable way that this can be fixed is to apply the same shader transformations to midTexCoord + // as Sodium does to the regular texture coordinates - dividing them by the correct power-of-two value inside + // of the shader instead of letting OpenGL value normalization do the division. However, this requires + // fragile patching that is not yet possible. + // + // As a temporary solution, the normalized shorts have been replaced with regular floats, but this takes up + // an extra 4 bytes per vertex. + + // NB: Be careful with the math here! A previous bug was caused by midU going negative as a short, which + // was sign-extended into midTexCoord, causing midV to have garbage (likely NaN data). If you're touching + // this code, be aware of that, and don't introduce those kinds of bugs! + // + // Also note that OpenGL takes shorts in the range of [0, 65535] and transforms them linearly to [0.0, 1.0], + // so multiply by 65535, not 65536. + // + // TODO: Does this introduce precision issues? Do we need to fall back to floats here? This might break + // with high resolution texture packs. +// int midU = (int)(65535.0F * Math.min(uSum * 0.25f, 1.0f)) & 0xFFFF; +// int midV = (int)(65535.0F * Math.min(vSum * 0.25f, 1.0f)) & 0xFFFF; +// int midTexCoord = (midV << 16) | midU; + + uSum *= 0.25f; + vSum *= 0.25f; + + buffer.putFloat(i + 20, uSum); + buffer.putFloat(i + 20 - STRIDE, uSum); + buffer.putFloat(i + 20 - STRIDE * 2, uSum); + buffer.putFloat(i + 20 - STRIDE * 3, uSum); + + buffer.putFloat(i + 24, vSum); + buffer.putFloat(i + 24 - STRIDE, vSum); + buffer.putFloat(i + 24 - STRIDE * 2, vSum); + buffer.putFloat(i + 24 - STRIDE * 3, vSum); + + uSum = 0; + vSum = 0; + + // normal computation + // Implementation based on the algorithm found here: + // https://github.com/IrisShaders/ShaderDoc/blob/master/vertex-format-extensions.md#surface-normal-vector + + quad.setup(buffer, i, STRIDE); + NormalHelper.computeFaceNormal(normal, quad); + int packedNormal = NormalHelper.packNormal(normal, 0.0f); + + buffer.putInt(i + 32, packedNormal); + buffer.putInt(i + 32 - STRIDE, packedNormal); + buffer.putInt(i + 32 - STRIDE * 2, packedNormal); + buffer.putInt(i + 32 - STRIDE * 3, packedNormal); + + int tangent = NormalHelper.computeTangent(normal.x, normal.y, normal.z, quad); + + buffer.putInt(i + 28, tangent); + buffer.putInt(i + 28 - STRIDE, tangent); + buffer.putInt(i + 28 - STRIDE * 2, tangent); + buffer.putInt(i + 28 - STRIDE * 3, tangent); + } + + this.advance(); + } + + @Override + public void iris$setContextHolder(BlockContextHolder holder) { + this.contextHolder = holder; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterUnsafe.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterUnsafe.java new file mode 100644 index 000000000..f9047c6a6 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexBufferWriterUnsafe.java @@ -0,0 +1,147 @@ +package net.coderbot.iris.sodium.vertex_format.terrain_xhfp; + +import com.gtnewhorizons.angelica.compat.lwjgl.CompatMemoryUtil; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferWriterUnsafe; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexUtil; +import net.coderbot.iris.sodium.block_context.BlockContextHolder; +import net.coderbot.iris.sodium.block_context.ContextAwareVertexWriter; +import net.coderbot.iris.sodium.vertex_format.IrisModelVertexFormats; +import net.coderbot.iris.vertices.ExtendedDataHelper; +import net.coderbot.iris.vertices.NormalHelper; +import org.joml.Vector3f; + +import static net.coderbot.iris.sodium.vertex_format.terrain_xhfp.XHFPModelVertexType.STRIDE; + +public class XHFPModelVertexBufferWriterUnsafe extends VertexBufferWriterUnsafe implements ModelVertexSink, ContextAwareVertexWriter { + private final QuadViewTerrain.QuadViewTerrainUnsafe quad = new QuadViewTerrain.QuadViewTerrainUnsafe(); + private final Vector3f normal = new Vector3f(); + + private BlockContextHolder contextHolder; + + private int vertexCount; + private float uSum; + private float vSum; + + public XHFPModelVertexBufferWriterUnsafe(VertexBufferView backingBuffer) { + super(backingBuffer, IrisModelVertexFormats.MODEL_VERTEX_XHFP); + } + + @Override + public void writeQuad(float x, float y, float z, int color, float u, float v, int light) { + uSum += u; + vSum += v; + + this.writeQuadInternal( + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(x), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(y), + ModelVertexUtil.denormalizeVertexPositionFloatAsShort(z), + color, + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(u), + ModelVertexUtil.denormalizeVertexTextureFloatAsShort(v), + light, + contextHolder.blockId, + contextHolder.renderType, + ExtendedDataHelper.computeMidBlock(x, y, z, contextHolder.localPosX, contextHolder.localPosY, contextHolder.localPosZ) + ); + } + + private void writeQuadInternal(short x, short y, short z, int color, short u, short v, int light, short materialId, + short renderType, int packedMidBlock) { + long i = this.writePointer; + + vertexCount++; + // NB: uSum and vSum must already be incremented outside of this function. + + CompatMemoryUtil.memPutShort(i, x); + CompatMemoryUtil.memPutShort(i + 2, y); + CompatMemoryUtil.memPutShort(i + 4, z); + CompatMemoryUtil.memPutInt(i + 8, color); + CompatMemoryUtil.memPutShort(i + 12, u); + CompatMemoryUtil.memPutShort(i + 14, v); + CompatMemoryUtil.memPutShort(i + 16, (short) (light & 0xFFFF)); + CompatMemoryUtil.memPutShort(i + 18, (short) (light >> 16 & 0xFFFF)); + // NB: We don't set midTexCoord, normal, and tangent here, they will be filled in later. + // block ID: We only set the first 2 values, any legacy shaders using z or w will get filled in based on the GLSL spec + // https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_format + // TODO: can we pack this into one short? + CompatMemoryUtil.memPutShort(i + 36, materialId); + CompatMemoryUtil.memPutShort(i + 38, renderType); + CompatMemoryUtil.memPutInt(i + 40, packedMidBlock); + + if (vertexCount == 4) { + vertexCount = 0; + + // FIXME + // The following logic is incorrect because OpenGL denormalizes shorts by dividing by 65535. The atlas is + // based on power-of-two values and so a normalization factor that is not a power of two causes the values + // used in the shader to be off by enough to cause visual errors. These are most noticeable on 1.18 with POM + // on block edges. + // + // The only reliable way that this can be fixed is to apply the same shader transformations to midTexCoord + // as Sodium does to the regular texture coordinates - dividing them by the correct power-of-two value inside + // of the shader instead of letting OpenGL value normalization do the division. However, this requires + // fragile patching that is not yet possible. + // + // As a temporary solution, the normalized shorts have been replaced with regular floats, but this takes up + // an extra 4 bytes per vertex. + + // NB: Be careful with the math here! A previous bug was caused by midU going negative as a short, which + // was sign-extended into midTexCoord, causing midV to have garbage (likely NaN data). If you're touching + // this code, be aware of that, and don't introduce those kinds of bugs! + // + // Also note that OpenGL takes shorts in the range of [0, 65535] and transforms them linearly to [0.0, 1.0], + // so multiply by 65535, not 65536. + // + // TODO: Does this introduce precision issues? Do we need to fall back to floats here? This might break + // with high resolution texture packs. +// int midU = (int)(65535.0F * Math.min(uSum * 0.25f, 1.0f)) & 0xFFFF; +// int midV = (int)(65535.0F * Math.min(vSum * 0.25f, 1.0f)) & 0xFFFF; +// int midTexCoord = (midV << 16) | midU; + + uSum *= 0.25f; + vSum *= 0.25f; + + CompatMemoryUtil.memPutFloat(i + 20, uSum); + CompatMemoryUtil.memPutFloat(i + 20 - STRIDE, uSum); + CompatMemoryUtil.memPutFloat(i + 20 - STRIDE * 2, uSum); + CompatMemoryUtil.memPutFloat(i + 20 - STRIDE * 3, uSum); + + CompatMemoryUtil.memPutFloat(i + 24, vSum); + CompatMemoryUtil.memPutFloat(i + 24 - STRIDE, vSum); + CompatMemoryUtil.memPutFloat(i + 24 - STRIDE * 2, vSum); + CompatMemoryUtil.memPutFloat(i + 24 - STRIDE * 3, vSum); + + uSum = 0; + vSum = 0; + + // normal computation + // Implementation based on the algorithm found here: + // https://github.com/IrisShaders/ShaderDoc/blob/master/vertex-format-extensions.md#surface-normal-vector + + quad.setup(i, STRIDE); + NormalHelper.computeFaceNormal(normal, quad); + int packedNormal = NormalHelper.packNormal(normal, 0.0f); + + CompatMemoryUtil.memPutInt(i + 32, packedNormal); + CompatMemoryUtil.memPutInt(i + 32 - STRIDE, packedNormal); + CompatMemoryUtil.memPutInt(i + 32 - STRIDE * 2, packedNormal); + CompatMemoryUtil.memPutInt(i + 32 - STRIDE * 3, packedNormal); + + int tangent = NormalHelper.computeTangent(normal.x, normal.y, normal.z, quad); + + CompatMemoryUtil.memPutInt(i + 28, tangent); + CompatMemoryUtil.memPutInt(i + 28 - STRIDE, tangent); + CompatMemoryUtil.memPutInt(i + 28 - STRIDE * 2, tangent); + CompatMemoryUtil.memPutInt(i + 28 - STRIDE * 3, tangent); + } + + this.advance(); + } + + @Override + public void iris$setContextHolder(BlockContextHolder holder) { + this.contextHolder = holder; + } +} diff --git a/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexType.java b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexType.java new file mode 100644 index 000000000..70e8fd6f3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/sodium/vertex_format/terrain_xhfp/XHFPModelVertexType.java @@ -0,0 +1,62 @@ +package net.coderbot.iris.sodium.vertex_format.terrain_xhfp; + +import com.gtnewhorizons.angelica.compat.toremove.VertexConsumer; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.model.vertex.buffer.VertexBufferView; +import me.jellysquid.mods.sodium.client.model.vertex.type.BlittableVertexType; +import me.jellysquid.mods.sodium.client.model.vertex.type.ChunkVertexType; +import me.jellysquid.mods.sodium.client.render.chunk.format.ChunkMeshAttribute; +import me.jellysquid.mods.sodium.client.render.chunk.format.ModelVertexSink; +import net.coderbot.iris.sodium.vertex_format.IrisGlVertexAttributeFormat; + +/** + * Like HFPModelVertexType, but extended to support Iris. The extensions aren't particularly efficient right now. + */ +public class XHFPModelVertexType implements ChunkVertexType { + public static final int STRIDE = 44; + public static final GlVertexFormat VERTEX_FORMAT = GlVertexFormat.builder(ChunkMeshAttribute.class, STRIDE) + .addElement(ChunkMeshAttribute.POSITION, 0, GlVertexAttributeFormat.UNSIGNED_SHORT, 3, false) + .addElement(ChunkMeshAttribute.COLOR, 8, GlVertexAttributeFormat.UNSIGNED_BYTE, 4, true) + .addElement(ChunkMeshAttribute.TEXTURE, 12, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, false) + .addElement(ChunkMeshAttribute.LIGHT, 16, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, false) + .addElement(ChunkMeshAttribute.MID_TEX_COORD, 20, GlVertexAttributeFormat.FLOAT, 2, false) + .addElement(ChunkMeshAttribute.TANGENT, 28, IrisGlVertexAttributeFormat.BYTE, 4, true) + .addElement(ChunkMeshAttribute.NORMAL, 32, IrisGlVertexAttributeFormat.BYTE, 3, true) + .addElement(ChunkMeshAttribute.BLOCK_ID, 36, IrisGlVertexAttributeFormat.SHORT, 2, false) + .addElement(ChunkMeshAttribute.MID_BLOCK, 40, IrisGlVertexAttributeFormat.BYTE, 3, false) + .build(); + + public static final float MODEL_SCALE = (32.0f / 65536.0f); + public static final float TEXTURE_SCALE = (1.0f / 32768.0f); + + @Override + public ModelVertexSink createFallbackWriter(VertexConsumer consumer) { + throw new UnsupportedOperationException(); + } + + @Override + public ModelVertexSink createBufferWriter(VertexBufferView buffer, boolean direct) { + return direct ? new XHFPModelVertexBufferWriterUnsafe(buffer) : new XHFPModelVertexBufferWriterNio(buffer); + } + + @Override + public BlittableVertexType asBlittable() { + return this; + } + + @Override + public GlVertexFormat getCustomVertexFormat() { + return VERTEX_FORMAT; + } + + @Override + public float getModelScale() { + return MODEL_SCALE; + } + + @Override + public float getTextureScale() { + return TEXTURE_SCALE; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/TextureInfoCache.java b/src/main/java/net/coderbot/iris/texture/TextureInfoCache.java new file mode 100644 index 000000000..da8a9a974 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/TextureInfoCache.java @@ -0,0 +1,100 @@ +package net.coderbot.iris.texture; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + +import java.nio.IntBuffer; + +public class TextureInfoCache { + public static final TextureInfoCache INSTANCE = new TextureInfoCache(); + + private final Int2ObjectMap cache = new Int2ObjectOpenHashMap<>(); + + private TextureInfoCache() { + } + + public TextureInfo getInfo(int id) { + TextureInfo info = cache.get(id); + if (info == null) { + info = new TextureInfo(id); + cache.put(id, info); + } + return info; + } + + public void onTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) { + if (level == 0) { + int id = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + TextureInfo info = getInfo(id); + info.internalFormat = internalformat; + info.width = width; + info.height = height; + } + } + public void onTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) { + if (level == 0) { + int id = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + TextureInfo info = getInfo(id); + info.internalFormat = internalformat; + info.width = width; + info.height = height; + } + } + + public void onDeleteTexture(int id) { + cache.remove(id); + } + + public static class TextureInfo { + private final int id; + private int internalFormat = -1; + private int width = -1; + private int height = -1; + + private TextureInfo(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public int getInternalFormat() { + if (internalFormat == -1) { + internalFormat = fetchLevelParameter(GL11.GL_TEXTURE_INTERNAL_FORMAT); + } + return internalFormat; + } + + public int getWidth() { + if (width == -1) { + width = fetchLevelParameter(GL11.GL_TEXTURE_WIDTH); + } + return width; + } + + public int getHeight() { + if (height == -1) { + height = fetchLevelParameter(GL11.GL_TEXTURE_HEIGHT); + } + return height; + } + + private int fetchLevelParameter(int pname) { + // Keep track of what texture was bound before + int previousTextureBinding = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + + // Bind this texture and grab the parameter from it. + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id); + int parameter = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, pname); + + // Make sure to re-bind the previous texture to avoid issues. + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, previousTextureBinding); + + return parameter; + } + } +} diff --git a/src/main/java/net/coderbot/iris/texture/TextureTracker.java b/src/main/java/net/coderbot/iris/texture/TextureTracker.java new file mode 100644 index 000000000..659d03457 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/TextureTracker.java @@ -0,0 +1,60 @@ +package net.coderbot.iris.texture; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.jetbrains.annotations.Nullable; + +public class TextureTracker { + public static final TextureTracker INSTANCE = new TextureTracker(); + + private static Runnable bindTextureListener; + + static { + StateUpdateNotifiers.bindTextureNotifier = listener -> bindTextureListener = listener; + } + + private final Int2ObjectMap textures = new Int2ObjectOpenHashMap<>(); + + private boolean lockBindCallback; + + private TextureTracker() { + } + + public void trackTexture(int id, AbstractTexture texture) { + textures.put(id, texture); + } + + @Nullable + public AbstractTexture getTexture(int id) { + return textures.get(id); + } + + public void onBindTexture(int id) { + if (lockBindCallback) { + return; + } + if (GLStateManager.getActiveTexture() == 0) { + lockBindCallback = true; + if (bindTextureListener != null) { + bindTextureListener.run(); + } + WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); + if (pipeline != null) { + pipeline.onBindTexture(id); + } + // Reset texture state + IrisRenderSystem.bindTextureToUnit(0, id); + lockBindCallback = false; + } + } + + public void onDeleteTexture(int id) { + textures.remove(id); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/format/LabPBRTextureFormat.java b/src/main/java/net/coderbot/iris/texture/format/LabPBRTextureFormat.java new file mode 100644 index 000000000..0687d3bb9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/format/LabPBRTextureFormat.java @@ -0,0 +1,71 @@ +package net.coderbot.iris.texture.format; + +import net.coderbot.iris.texture.mipmap.ChannelMipmapGenerator; +import net.coderbot.iris.texture.mipmap.CustomMipmapGenerator; +import net.coderbot.iris.texture.mipmap.DiscreteBlendFunction; +import net.coderbot.iris.texture.mipmap.LinearBlendFunction; +import net.coderbot.iris.texture.pbr.PBRType; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class LabPBRTextureFormat implements TextureFormat { + public static final ChannelMipmapGenerator SPECULAR_MIPMAP_GENERATOR = new ChannelMipmapGenerator( + LinearBlendFunction.INSTANCE, + new DiscreteBlendFunction(v -> v < 230 ? 0 : v - 229), + new DiscreteBlendFunction(v -> v < 65 ? 0 : 1), + new DiscreteBlendFunction(v -> v < 255 ? 0 : 1) + ); + + private final String name; + @Nullable + private final String version; + + public LabPBRTextureFormat(String name, @Nullable String version) { + this.name = name; + this.version = version; + } + + @Override + public String getName() { + return name; + } + + @Override + public @Nullable String getVersion() { + return version; + } + + @Override + public boolean canInterpolateValues(PBRType pbrType) { + if (pbrType == PBRType.SPECULAR) { + return false; + } + return true; + } + + @Override + public @Nullable CustomMipmapGenerator getMipmapGenerator(PBRType pbrType) { + if (pbrType == PBRType.SPECULAR) { + return SPECULAR_MIPMAP_GENERATOR; + } + return null; + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LabPBRTextureFormat other = (LabPBRTextureFormat) obj; + return Objects.equals(name, other.name) && Objects.equals(version, other.version); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/format/TextureFormat.java b/src/main/java/net/coderbot/iris/texture/format/TextureFormat.java new file mode 100644 index 000000000..603665dd8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/format/TextureFormat.java @@ -0,0 +1,64 @@ +package net.coderbot.iris.texture.format; + +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.texture.mipmap.CustomMipmapGenerator; +import net.coderbot.iris.texture.pbr.PBRType; +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public interface TextureFormat { + String getName(); + + @Nullable + String getVersion(); + + default List getDefines() { + List defines = new ArrayList<>(); + + String defineName = getName().toUpperCase(Locale.ROOT).replaceAll("-", "_"); + String define = "MC_TEXTURE_FORMAT_" + defineName; + defines.add(define); + + String version = getVersion(); + if (version != null) { + String defineVersion = version.replaceAll("[.-]", "_"); + String versionDefine = define + "_" + defineVersion; + defines.add(versionDefine); + } + + return defines; + } + + /** + * Dictates whether textures of the given PBR type can have their color values interpolated or not. + * Usually, this controls the texture minification and magification filters - + * a return value of false would signify that the linear filters cannot be used. + * + * @param pbrType The type of PBR texture + * @return If texture values can be interpolated or not + */ + boolean canInterpolateValues(PBRType pbrType); + + default void setupTextureParameters(PBRType pbrType, AbstractTexture texture) { + if (!canInterpolateValues(pbrType)) { + int minFilter = IrisRenderSystem.getTexParameteri(texture.getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); + // Non-mipped filters begin at 0x2600 whereas mipped filters begin at 0x2700, + // so this bit mask can be used to check if the filter is mipped or not + boolean mipmap = (minFilter & 1 << 8) == 1; + IrisRenderSystem.texParameteri(texture.getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, mipmap ? GL11.GL_NEAREST_MIPMAP_NEAREST : GL11.GL_NEAREST); + IrisRenderSystem.texParameteri(texture.getGlTextureId(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + } + } + + @Nullable + CustomMipmapGenerator getMipmapGenerator(PBRType pbrType); + + public interface Factory { + TextureFormat createFormat(String name, @Nullable String version); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/format/TextureFormatLoader.java b/src/main/java/net/coderbot/iris/texture/format/TextureFormatLoader.java new file mode 100644 index 000000000..3dbdffb63 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/format/TextureFormatLoader.java @@ -0,0 +1,73 @@ +package net.coderbot.iris.texture.format; + +import net.coderbot.iris.Iris; +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Objects; +import java.util.Properties; + +public class TextureFormatLoader { + public static final ResourceLocation LOCATION = new ResourceLocation("optifine/texture.properties"); + + private static TextureFormat format; + + @Nullable + public static TextureFormat getFormat() { + return format; + } + + public static void reload(IResourceManager resourceManager) { + TextureFormat newFormat = loadFormat(resourceManager); + boolean didFormatChange = !Objects.equals(format, newFormat); + format = newFormat; + if (didFormatChange) { + onFormatChange(); + } + } + + @Nullable + private static TextureFormat loadFormat(IResourceManager resourceManager) { + try { + IResource resource = resourceManager.getResource(LOCATION); + Properties properties = new Properties(); + properties.load(resource.getInputStream()); + String format = properties.getProperty("format"); + if (format != null && !format.isEmpty()) { + String[] splitFormat = format.split("/"); + if (splitFormat.length > 0) { + String name = splitFormat[0]; + TextureFormat.Factory factory = TextureFormatRegistry.INSTANCE.getFactory(name); + if (factory != null) { + String version; + if (splitFormat.length > 1) { + version = splitFormat[1]; + } else { + version = null; + } + return factory.createFormat(name, version); + } else { + Iris.logger.warn("Invalid texture format '" + name + "' in file '" + LOCATION + "'"); + } + } + } + } catch (FileNotFoundException e) { + // + } catch (Exception e) { + Iris.logger.error("Failed to load texture format from file '" + LOCATION + "'", e); + } + return null; + } + + private static void onFormatChange() { + try { + Iris.reload(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/coderbot/iris/texture/format/TextureFormatRegistry.java b/src/main/java/net/coderbot/iris/texture/format/TextureFormatRegistry.java new file mode 100644 index 000000000..513524d67 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/format/TextureFormatRegistry.java @@ -0,0 +1,25 @@ +package net.coderbot.iris.texture.format; + +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class TextureFormatRegistry { + public static final TextureFormatRegistry INSTANCE = new TextureFormatRegistry(); + + static { + INSTANCE.register("lab-pbr", LabPBRTextureFormat::new); + } + + private final Map factoryMap = new HashMap<>(); + + public void register(String name, TextureFormat.Factory factory) { + factoryMap.put(name, factory); + } + + @Nullable + public TextureFormat.Factory getFactory(String name) { + return factoryMap.get(name); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/mipmap/AbstractMipmapGenerator.java b/src/main/java/net/coderbot/iris/texture/mipmap/AbstractMipmapGenerator.java new file mode 100644 index 000000000..81d1e4ba8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/mipmap/AbstractMipmapGenerator.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.texture.mipmap; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; + +public abstract class AbstractMipmapGenerator implements CustomMipmapGenerator { + @Override + public NativeImage[] generateMipLevels(NativeImage image, int mipLevel) { + NativeImage[] images = new NativeImage[mipLevel + 1]; + images[0] = image; + if (mipLevel > 0) { + for (int level = 1; level <= mipLevel; ++level) { + NativeImage prevMipmap = images[level - 1]; + NativeImage mipmap = new NativeImage(prevMipmap.getWidth() >> 1, prevMipmap.getHeight() >> 1, false); + int width = mipmap.getWidth(); + int height = mipmap.getHeight(); + for (int x = 0; x < width; ++x) { + for (int y = 0; y < height; ++y) { + mipmap.setPixelRGBA(x, y, blend( + prevMipmap.getPixelRGBA(x * 2 + 0, y * 2 + 0), + prevMipmap.getPixelRGBA(x * 2 + 1, y * 2 + 0), + prevMipmap.getPixelRGBA(x * 2 + 0, y * 2 + 1), + prevMipmap.getPixelRGBA(x * 2 + 1, y * 2 + 1) + )); + } + } + images[level] = mipmap; + } + } + return images; + } + + public abstract int blend(int c0, int c1, int c2, int c3); +} diff --git a/src/main/java/net/coderbot/iris/texture/mipmap/ChannelMipmapGenerator.java b/src/main/java/net/coderbot/iris/texture/mipmap/ChannelMipmapGenerator.java new file mode 100644 index 000000000..dd92ad317 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/mipmap/ChannelMipmapGenerator.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.texture.mipmap; +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; + +public class ChannelMipmapGenerator extends AbstractMipmapGenerator { + protected final BlendFunction redFunc; + protected final BlendFunction greenFunc; + protected final BlendFunction blueFunc; + protected final BlendFunction alphaFunc; + + public ChannelMipmapGenerator(BlendFunction redFunc, BlendFunction greenFunc, BlendFunction blueFunc, BlendFunction alphaFunc) { + this.redFunc = redFunc; + this.greenFunc = greenFunc; + this.blueFunc = blueFunc; + this.alphaFunc = alphaFunc; + } + + @Override + public int blend(int c0, int c1, int c2, int c3) { + return NativeImage.combine( + alphaFunc.blend(NativeImage.getA(c0), NativeImage.getA(c1), NativeImage.getA(c2), NativeImage.getA(c3)), + blueFunc.blend(NativeImage.getB(c0), NativeImage.getB(c1), NativeImage.getB(c2), NativeImage.getB(c3)), + greenFunc.blend(NativeImage.getG(c0), NativeImage.getG(c1), NativeImage.getG(c2), NativeImage.getG(c3)), + redFunc.blend(NativeImage.getR(c0), NativeImage.getR(c1), NativeImage.getR(c2), NativeImage.getR(c3)) + ); + } + + public interface BlendFunction { + int blend(int v0, int v1, int v2, int v3); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/mipmap/CustomMipmapGenerator.java b/src/main/java/net/coderbot/iris/texture/mipmap/CustomMipmapGenerator.java new file mode 100644 index 000000000..acb5b1ef7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/mipmap/CustomMipmapGenerator.java @@ -0,0 +1,15 @@ +package net.coderbot.iris.texture.mipmap; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import net.coderbot.iris.texture.pbr.loader.TextureAtlasSpriteInfo; + +import javax.annotation.Nullable; + +public interface CustomMipmapGenerator { + NativeImage[] generateMipLevels(NativeImage image, int mipLevel); + + public interface Provider { + @Nullable + CustomMipmapGenerator getMipmapGenerator(TextureAtlasSpriteInfo info, int atlasWidth, int atlasHeight); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/mipmap/DiscreteBlendFunction.java b/src/main/java/net/coderbot/iris/texture/mipmap/DiscreteBlendFunction.java new file mode 100644 index 000000000..9abc05879 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/mipmap/DiscreteBlendFunction.java @@ -0,0 +1,58 @@ +package net.coderbot.iris.texture.mipmap; + +import java.util.function.IntUnaryOperator; + +public class DiscreteBlendFunction implements ChannelMipmapGenerator.BlendFunction { + protected final IntUnaryOperator typeFunc; + + public DiscreteBlendFunction(IntUnaryOperator typeFunc) { + this.typeFunc = typeFunc; + } + + @Override + public int blend(int v0, int v1, int v2, int v3) { + int t0 = typeFunc.applyAsInt(v0); + int t1 = typeFunc.applyAsInt(v1); + int t2 = typeFunc.applyAsInt(v2); + int t3 = typeFunc.applyAsInt(v3); + + int targetType = selectTargetType(t0, t1, t2, t3); + + int sum = 0; + int amount = 0; + if (t0 == targetType) { + sum += v0; + amount++; + } + if (t1 == targetType) { + sum += v1; + amount++; + } + if (t2 == targetType) { + sum += v2; + amount++; + } + if (t3 == targetType) { + sum += v3; + amount++; + } + + return sum / amount; + } + + /** + * Selects the type that appears most often among the arguments. + * In the case of a tie, types that appear first in the argument list are given priority. + * A type is represented by an integer. + * */ + public static int selectTargetType(int t0, int t1, int t2, int t3) { + if (t0 != t1 && t0 != t2) { + if (t2 == t3) { + return t2; + } else if (t0 != t3 && (t1 == t2 || t1 == t3)) { + return t1; + } + } + return t0; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/mipmap/LinearBlendFunction.java b/src/main/java/net/coderbot/iris/texture/mipmap/LinearBlendFunction.java new file mode 100644 index 000000000..22faccc7b --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/mipmap/LinearBlendFunction.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.texture.mipmap; + +public class LinearBlendFunction implements ChannelMipmapGenerator.BlendFunction { + public static final LinearBlendFunction INSTANCE = new LinearBlendFunction(); + + @Override + public int blend(int v0, int v1, int v2, int v3) { + return (v0 + v1 + v2 + v3) / 4; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasHolder.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasHolder.java new file mode 100644 index 000000000..49e781712 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasHolder.java @@ -0,0 +1,35 @@ +package net.coderbot.iris.texture.pbr; + +import org.jetbrains.annotations.Nullable; + +public class PBRAtlasHolder { + protected PBRAtlasTexture normalAtlas; + protected PBRAtlasTexture specularAtlas; + + @Nullable + public PBRAtlasTexture getNormalAtlas() { + return normalAtlas; + } + + @Nullable + public PBRAtlasTexture getSpecularAtlas() { + return specularAtlas; + } + + public void setNormalAtlas(PBRAtlasTexture atlas) { + normalAtlas = atlas; + } + + public void setSpecularAtlas(PBRAtlasTexture atlas) { + specularAtlas = atlas; + } + + public void cycleAnimationFrames() { + if (normalAtlas != null) { + normalAtlas.cycleAnimationFrames(); + } + if (specularAtlas != null) { + specularAtlas.cycleAnimationFrames(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java new file mode 100644 index 000000000..c9a454239 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java @@ -0,0 +1,144 @@ +package net.coderbot.iris.texture.pbr; + +import com.gtnewhorizons.angelica.compat.mojang.AutoClosableAbstractTexture; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.TextureAtlasSpriteAccessor; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.TextureMapAccessor; +import lombok.Getter; +import net.coderbot.iris.texture.util.TextureExporter; +import net.coderbot.iris.texture.util.TextureManipulationUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.texture.TextureUtil; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class PBRAtlasTexture extends AutoClosableAbstractTexture { + protected final TextureMap texMap; + @Getter + protected final PBRType type; + protected final ResourceLocation id; + protected final Map sprites = new HashMap<>(); + protected final Set animatedSprites = new HashSet<>(); + + public PBRAtlasTexture(TextureMap textureMap, PBRType type) { + this.texMap = textureMap; + this.type = type; + id = type.appendToFileLocation(((TextureMapAccessor)textureMap).getLocationBlocksTexture()); + + } + + public ResourceLocation getAtlasId() { + return id; + } + + public void addSprite(TextureAtlasSprite sprite) { + sprites.put(texMap.completeResourceLocation(new ResourceLocation(sprite.getIconName()), 0), sprite); + if (sprite.hasAnimationMetadata()) { + animatedSprites.add(sprite); + } + } + + @Nullable + public TextureAtlasSprite getSprite(ResourceLocation id) { + return sprites.get(id); + } + + public void clear() { + sprites.clear(); + animatedSprites.clear(); + } + + public void upload(int atlasWidth, int atlasHeight, int mipLevel, float anisotropicFiltering) { + final int glId = getGlTextureId(); + TextureUtil.allocateTextureImpl(glId, mipLevel, atlasWidth, atlasHeight, anisotropicFiltering); + TextureManipulationUtil.fillWithColor(glId, mipLevel, type.getDefaultValue()); + + for (TextureAtlasSprite sprite : sprites.values()) { + try { + uploadSprite(sprite); + } catch (Throwable throwable) { + CrashReport crashReport = CrashReport.makeCrashReport(throwable, "Stitching texture atlas"); + CrashReportCategory crashReportCategory = crashReport.makeCategory("Texture being stitched together"); + crashReportCategory.addCrashSection("Atlas path", id); + crashReportCategory.addCrashSection("Sprite", sprite); + throw new ReportedException(crashReport); + } + } + + if (!animatedSprites.isEmpty()) { + final PBRAtlasHolder pbrHolder = ((TextureAtlasExtension) texMap).getOrCreatePBRHolder(); + switch (type) { + case NORMAL: + pbrHolder.setNormalAtlas(this); + break; + case SPECULAR: + pbrHolder.setSpecularAtlas(this); + break; + } + } + + if (PBRTextureManager.DEBUG) { + TextureExporter.exportTextures("pbr_debug/atlas", id.getResourceDomain() + "_" + id.getResourcePath().replaceAll("/", "_"), glId, mipLevel, atlasWidth, atlasHeight); + } + } + + public boolean tryUpload(int atlasWidth, int atlasHeight, int mipLevel, float anisotropicFiltering) { + try { + upload(atlasWidth, atlasHeight, mipLevel, anisotropicFiltering); + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public void loadTexture(IResourceManager manager) throws IOException { + // todo + } + + protected void uploadSprite(TextureAtlasSprite sprite) { + + final TextureAtlasSpriteAccessor accessor = (TextureAtlasSpriteAccessor) sprite; + if (accessor.getMetadata().getFrameCount() > 1) { + final AnimationMetadataSection metadata = accessor.getMetadata(); + final int frameCount = sprite.getFrameCount(); + for (int frame = accessor.getFrame(); frame >= 0; frame--) { + final int frameIndex = metadata.getFrameIndex(frame); + if (frameIndex >= 0 && frameIndex < frameCount) { + TextureUtil.uploadTextureMipmap(sprite.getFrameTextureData(frameIndex), sprite.getIconWidth(), sprite.getIconHeight(), sprite.getOriginX(), sprite.getOriginY(), false, false); + return; + } + } + } + TextureUtil.uploadTextureMipmap(sprite.getFrameTextureData(0), sprite.getIconWidth(), sprite.getIconHeight(), sprite.getOriginX(), sprite.getOriginY(), false, false); + } + + public void cycleAnimationFrames() { + bind(); + for (TextureAtlasSprite sprite : animatedSprites) { + sprite.updateAnimation(); + } + } + + @Override + public void close() { + final PBRAtlasHolder pbrHolder = ((TextureAtlasExtension) texMap).getPBRHolder(); + if (pbrHolder != null) { + switch (type) { + case NORMAL -> pbrHolder.setNormalAtlas(null); + case SPECULAR -> pbrHolder.setSpecularAtlas(null); + } + } + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRSpriteHolder.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRSpriteHolder.java new file mode 100644 index 000000000..97ed0bffb --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRSpriteHolder.java @@ -0,0 +1,27 @@ +package net.coderbot.iris.texture.pbr; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.jetbrains.annotations.Nullable; + +public class PBRSpriteHolder { + protected TextureAtlasSprite normalSprite; + protected TextureAtlasSprite specularSprite; + + @Nullable + public TextureAtlasSprite getNormalSprite() { + return normalSprite; + } + + @Nullable + public TextureAtlasSprite getSpecularSprite() { + return specularSprite; + } + + public void setNormalSprite(TextureAtlasSprite sprite) { + normalSprite = sprite; + } + + public void setSpecularSprite(TextureAtlasSprite sprite) { + specularSprite = sprite; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureHolder.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureHolder.java new file mode 100644 index 000000000..defaf949f --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureHolder.java @@ -0,0 +1,12 @@ +package net.coderbot.iris.texture.pbr; + +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.jetbrains.annotations.NotNull; + +public interface PBRTextureHolder { + @NotNull + AbstractTexture getNormalTexture(); + + @NotNull + AbstractTexture getSpecularTexture(); +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureManager.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureManager.java new file mode 100644 index 000000000..acb8da066 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRTextureManager.java @@ -0,0 +1,195 @@ +package net.coderbot.iris.texture.pbr; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.rendertarget.NativeImageBackedSingleColorTexture; +import net.coderbot.iris.texture.TextureTracker; +import net.coderbot.iris.texture.pbr.loader.PBRTextureLoader; +import net.coderbot.iris.texture.pbr.loader.PBRTextureLoader.PBRTextureConsumer; +import net.coderbot.iris.texture.pbr.loader.PBRTextureLoaderRegistry; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +public class PBRTextureManager { + public static final PBRTextureManager INSTANCE = new PBRTextureManager(); + + public static final boolean DEBUG = System.getProperty("iris.pbr.debug") != null; + + // TODO: Figure out how to merge these two. + private static Runnable normalTextureChangeListener; + private static Runnable specularTextureChangeListener; + + static { + StateUpdateNotifiers.normalTextureChangeNotifier = listener -> normalTextureChangeListener = listener; + StateUpdateNotifiers.specularTextureChangeNotifier = listener -> specularTextureChangeListener = listener; + } + + private final Int2ObjectMap holders = new Int2ObjectOpenHashMap<>(); + private final PBRTextureConsumerImpl consumer = new PBRTextureConsumerImpl(); + + private NativeImageBackedSingleColorTexture defaultNormalTexture; + private NativeImageBackedSingleColorTexture defaultSpecularTexture; + // Not PBRTextureHolderImpl to directly reference fields + private final PBRTextureHolder defaultHolder = new PBRTextureHolder() { + @Override + public @NotNull AbstractTexture getNormalTexture() { + return defaultNormalTexture; + } + + @Override + public @NotNull AbstractTexture getSpecularTexture() { + return defaultSpecularTexture; + } + }; + + private PBRTextureManager() { + } + + public void init() { + defaultNormalTexture = new NativeImageBackedSingleColorTexture(PBRType.NORMAL.getDefaultValue()); + defaultSpecularTexture = new NativeImageBackedSingleColorTexture(PBRType.SPECULAR.getDefaultValue()); + } + + public PBRTextureHolder getHolder(int id) { + PBRTextureHolder holder = holders.get(id); + if (holder == null) { + return defaultHolder; + } + return holder; + } + + public PBRTextureHolder getOrLoadHolder(int id) { + PBRTextureHolder holder = holders.get(id); + if (holder == null) { + holder = loadHolder(id); + holders.put(id, holder); + } + return holder; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private PBRTextureHolder loadHolder(int id) { + AbstractTexture texture = TextureTracker.INSTANCE.getTexture(id); + if (texture != null) { + Class clazz = texture.getClass(); + PBRTextureLoader loader = PBRTextureLoaderRegistry.INSTANCE.getLoader(clazz); + if (loader != null) { + int previousTextureBinding = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + consumer.clear(); + try { + loader.load(texture, Minecraft.getMinecraft().getResourceManager(), consumer); + return consumer.toHolder(); + } catch (Exception e) { + Iris.logger.debug("Failed to load PBR textures for texture " + id, e); + } finally { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, previousTextureBinding); + } + } + } + return defaultHolder; + } + + public void onDeleteTexture(int id) { + PBRTextureHolder holder = holders.remove(id); + if (holder != null) { + closeHolder(holder); + } + } + + public void clear() { + for (PBRTextureHolder holder : holders.values()) { + if (holder != defaultHolder) { + closeHolder(holder); + } + } + holders.clear(); + } + + public void close() { + clear(); +// defaultNormalTexture.close(); +// defaultSpecularTexture.close(); + } + + private void closeHolder(PBRTextureHolder holder) { + AbstractTexture normalTexture = holder.getNormalTexture(); + AbstractTexture specularTexture = holder.getSpecularTexture(); + if (normalTexture != defaultNormalTexture) { + closeTexture(normalTexture); + } + if (specularTexture != defaultSpecularTexture) { + closeTexture(specularTexture); + } + } + + private static void closeTexture(AbstractTexture texture) { + texture.deleteGlTexture(); + } + + public static void notifyPBRTexturesChanged() { + if (normalTextureChangeListener != null) { + normalTextureChangeListener.run(); + } + + if (specularTextureChangeListener != null) { + specularTextureChangeListener.run(); + } + } + + private class PBRTextureConsumerImpl implements PBRTextureConsumer { + private AbstractTexture normalTexture; + private AbstractTexture specularTexture; + private boolean changed; + + @Override + public void acceptNormalTexture(@NotNull AbstractTexture texture) { + normalTexture = texture; + changed = true; + } + + @Override + public void acceptSpecularTexture(@NotNull AbstractTexture texture) { + specularTexture = texture; + changed = true; + } + + public void clear() { + normalTexture = defaultNormalTexture; + specularTexture = defaultSpecularTexture; + changed = false; + } + + public PBRTextureHolder toHolder() { + if (changed) { + return new PBRTextureHolderImpl(normalTexture, specularTexture); + } else { + return defaultHolder; + } + } + } + + private static class PBRTextureHolderImpl implements PBRTextureHolder { + private final AbstractTexture normalTexture; + private final AbstractTexture specularTexture; + + public PBRTextureHolderImpl(AbstractTexture normalTexture, AbstractTexture specularTexture) { + this.normalTexture = normalTexture; + this.specularTexture = specularTexture; + } + + @Override + public @NotNull AbstractTexture getNormalTexture() { + return normalTexture; + } + + @Override + public @NotNull AbstractTexture getSpecularTexture() { + return specularTexture; + } + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRType.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRType.java new file mode 100644 index 000000000..ec5d3fb2e --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRType.java @@ -0,0 +1,56 @@ +package net.coderbot.iris.texture.pbr; + +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.FilenameUtils; +import org.jetbrains.annotations.Nullable; + +public enum PBRType { + NORMAL("_n", 0x7F7FFFFF), + SPECULAR("_s", 0x00000000); + + private static final PBRType[] VALUES = values(); + + private final String suffix; + private final int defaultValue; + + PBRType(String suffix, int defaultValue) { + this.suffix = suffix; + this.defaultValue = defaultValue; + } + + public String getSuffix() { + return suffix; + } + + public int getDefaultValue() { + return defaultValue; + } + + public ResourceLocation appendToFileLocation(ResourceLocation location) { + String path = location.getResourcePath(); + String newPath; + int extensionIndex = FilenameUtils.indexOfExtension(path); + if (extensionIndex != -1) { + newPath = path.substring(0, extensionIndex) + suffix + path.substring(extensionIndex); + } else { + newPath = path + suffix; + } + return new ResourceLocation(location.getResourceDomain(), newPath); + } + + /** + * Returns the PBR type corresponding to the suffix of the given file location. + * + * @param location The file location without an extension + * @return the PBR type + */ + @Nullable + public static PBRType fromFileLocation(String location) { + for (PBRType type : VALUES) { + if (location.endsWith(type.getSuffix())) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasExtension.java b/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasExtension.java new file mode 100644 index 000000000..d8ce9fcba --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasExtension.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.texture.pbr; + +import org.jetbrains.annotations.Nullable; + +public interface TextureAtlasExtension { + @Nullable + PBRAtlasHolder getPBRHolder(); + + PBRAtlasHolder getOrCreatePBRHolder(); +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasSpriteExtension.java b/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasSpriteExtension.java new file mode 100644 index 000000000..7a46ba5bc --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/TextureAtlasSpriteExtension.java @@ -0,0 +1,10 @@ +package net.coderbot.iris.texture.pbr; + +import org.jetbrains.annotations.Nullable; + +public interface TextureAtlasSpriteExtension { + @Nullable + PBRSpriteHolder getPBRHolder(); + + PBRSpriteHolder getOrCreatePBRHolder(); +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java new file mode 100644 index 000000000..e239daf3a --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java @@ -0,0 +1,265 @@ +package net.coderbot.iris.texture.pbr.loader; + +import com.google.common.collect.Lists; +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.AnimationMetadataSectionAccessor; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.TextureAtlasSpriteAccessor; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.TextureMapAccessor; +import net.coderbot.iris.Iris; +import net.coderbot.iris.texture.TextureInfoCache; +import net.coderbot.iris.texture.format.TextureFormat; +import net.coderbot.iris.texture.format.TextureFormatLoader; +import net.coderbot.iris.texture.mipmap.ChannelMipmapGenerator; +import net.coderbot.iris.texture.mipmap.CustomMipmapGenerator; +import net.coderbot.iris.texture.mipmap.LinearBlendFunction; +import net.coderbot.iris.texture.pbr.PBRAtlasTexture; +import net.coderbot.iris.texture.pbr.PBRSpriteHolder; +import net.coderbot.iris.texture.pbr.PBRType; +import net.coderbot.iris.texture.pbr.TextureAtlasSpriteExtension; +import net.coderbot.iris.texture.util.ImageManipulationUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nullable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collection; + + +public class AtlasPBRLoader implements PBRTextureLoader { + public static final ChannelMipmapGenerator LINEAR_MIPMAP_GENERATOR = new ChannelMipmapGenerator( + LinearBlendFunction.INSTANCE, + LinearBlendFunction.INSTANCE, + LinearBlendFunction.INSTANCE, + LinearBlendFunction.INSTANCE + ); + + @Override + public void load(TextureMap texMap, IResourceManager resourceManager, PBRTextureConsumer pbrTextureConsumer) { + final TextureInfoCache.TextureInfo textureInfo = TextureInfoCache.INSTANCE.getInfo(texMap.getGlTextureId()); + final int atlasWidth = textureInfo.getWidth(); + final int atlasHeight = textureInfo.getHeight(); + final int mipLevel = fetchAtlasMipLevel(texMap); + + PBRAtlasTexture normalAtlas = null; + PBRAtlasTexture specularAtlas = null; + for (TextureAtlasSprite sprite : (Collection) ((TextureMapAccessor) texMap).getMapUploadedSprites().values()) { + if (!(sprite.getIconName().equals("missingno"))) { + TextureAtlasSprite normalSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.NORMAL); + TextureAtlasSprite specularSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.SPECULAR); + if (normalSprite != null) { + if (normalAtlas == null) { + normalAtlas = new PBRAtlasTexture(texMap, PBRType.NORMAL); + } + normalAtlas.addSprite(normalSprite); + final PBRSpriteHolder pbrSpriteHolder = ((TextureAtlasSpriteExtension) sprite).getOrCreatePBRHolder(); + pbrSpriteHolder.setNormalSprite(normalSprite); + } + if (specularSprite != null) { + if (specularAtlas == null) { + specularAtlas = new PBRAtlasTexture(texMap, PBRType.SPECULAR); + } + specularAtlas.addSprite(specularSprite); + final PBRSpriteHolder pbrSpriteHolder = ((TextureAtlasSpriteExtension) sprite).getOrCreatePBRHolder(); + pbrSpriteHolder.setSpecularSprite(specularSprite); + } + } + } + + if (normalAtlas != null) { + if (normalAtlas.tryUpload(atlasWidth, atlasHeight, mipLevel, ((TextureMapAccessor) texMap).getAnisotropicFiltering())) { + pbrTextureConsumer.acceptNormalTexture(normalAtlas); + } + } + if (specularAtlas != null) { + if (specularAtlas.tryUpload(atlasWidth, atlasHeight, mipLevel, ((TextureMapAccessor) texMap).getAnisotropicFiltering())) { + pbrTextureConsumer.acceptSpecularTexture(specularAtlas); + } + } + } + + protected static int fetchAtlasMipLevel(TextureMap texMap) { + return ((TextureMapAccessor) texMap).getMipmapLevels(); + } + + @Nullable + protected TextureAtlasSprite createPBRSprite(TextureAtlasSprite sprite, IResourceManager resourceManager, TextureMap texMap, int atlasWidth, int atlasHeight, int mipLevel, PBRType pbrType) { + final ResourceLocation spriteName = new ResourceLocation(sprite.getIconName()); + final ResourceLocation imageLocation = texMap.completeResourceLocation(spriteName, 0); + final ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation); + + TextureAtlasSprite pbrSprite = null; + + try { + // This is no longer closable. Not sure about this. + final IResource resource = resourceManager.getResource(pbrImageLocation); + NativeImage nativeImage = NativeImage.read(resource.getInputStream()); + AnimationMetadataSection animationMetadata = (AnimationMetadataSection) resource.getMetadata("animation"); + if (animationMetadata == null) { + animationMetadata = new AnimationMetadataSection(Lists.newArrayList(), -1, -1, -1); + } + + final Pair frameSize = this.getFrameSize(nativeImage.getWidth(), nativeImage.getHeight(), animationMetadata); + int frameWidth = frameSize.getLeft(); + int frameHeight = frameSize.getRight(); + final int targetFrameWidth = sprite.getIconWidth(); + final int targetFrameHeight = sprite.getIconHeight(); + if (frameWidth != targetFrameWidth || frameHeight != targetFrameHeight) { + final int imageWidth = nativeImage.getWidth(); + final int imageHeight = nativeImage.getHeight(); + + // We can assume the following is always true as a result of getFrameSize's check: + // imageWidth % frameWidth == 0 && imageHeight % frameHeight == 0 + final int targetImageWidth = imageWidth / frameWidth * targetFrameWidth; + final int targetImageHeight = imageHeight / frameHeight * targetFrameHeight; + + final NativeImage scaledImage; + if (targetImageWidth % imageWidth == 0 && targetImageHeight % imageHeight == 0) { + scaledImage = ImageManipulationUtil.scaleNearestNeighbor(nativeImage, targetImageWidth, targetImageHeight); + } else { + scaledImage = ImageManipulationUtil.scaleBilinear(nativeImage, targetImageWidth, targetImageHeight); + } + + // This is no longer closeable either +// nativeImage.close(); + nativeImage = scaledImage; + + frameWidth = targetFrameWidth; + frameHeight = targetFrameHeight; + + if (!animationMetadata.equals(new AnimationMetadataSection(Lists.newArrayList(), -1, -1, -1))) { + final AnimationMetadataSectionAccessor animationAccessor = (AnimationMetadataSectionAccessor) animationMetadata; + final int internalFrameWidth = animationAccessor.getFrameHeight(); + final int internalFrameHeight = animationAccessor.getFrameHeight(); + if (internalFrameWidth != -1) { + animationAccessor.setFrameWidth(frameWidth); + } + if (internalFrameHeight != -1) { + animationAccessor.setFrameHeight(frameHeight); + } + } + } + + final ResourceLocation pbrSpriteName = new ResourceLocation(spriteName.getResourceDomain(), spriteName.getResourcePath() + pbrType.getSuffix()); + final TextureAtlasSpriteInfo pbrSpriteInfo = new PBRTextureAtlasSpriteInfo(pbrSpriteName, frameWidth, frameHeight, animationMetadata, pbrType); + + final int x = sprite.getOriginX(); + final int y = sprite.getOriginY(); + pbrSprite = new PBRTextureAtlasSprite(pbrSpriteInfo, animationMetadata, atlasWidth, atlasHeight, x, y, nativeImage, texMap, mipLevel); + syncAnimation(sprite, pbrSprite); + } catch (FileNotFoundException e) { + // + } catch (RuntimeException e) { + Iris.logger.error("Unable to parse metadata from {} : {}", pbrImageLocation, e); + } catch (IOException e) { + Iris.logger.error("Unable to load {} : {}", pbrImageLocation, e); + } + + return pbrSprite; + } + + + protected void syncAnimation(TextureAtlasSprite source, TextureAtlasSprite target) { + if (!((ISpriteExt)source).isAnimation() || !((ISpriteExt)target).isAnimation()) { + return; + } + + final TextureAtlasSpriteAccessor sourceAccessor = ((TextureAtlasSpriteAccessor) source); + final AnimationMetadataSection sourceMetadata = sourceAccessor.getMetadata(); + + int ticks = 0; + for (int f = 0; f < sourceAccessor.getFrame(); f++) { + ticks += sourceMetadata.getFrameTimeSingle(f); + } + + final TextureAtlasSpriteAccessor targetAccessor = ((TextureAtlasSpriteAccessor) target); + final AnimationMetadataSection targetMetadata = targetAccessor.getMetadata(); + + int cycleTime = 0; + final int frameCount = targetMetadata.getFrameCount(); + for (int f = 0; f < frameCount; f++) { + cycleTime += targetMetadata.getFrameTimeSingle(f); + } + ticks %= cycleTime; + + int targetFrame = 0; + while (true) { + final int time = targetMetadata.getFrameTimeSingle(targetFrame); + if (ticks >= time) { + targetFrame++; + ticks -= time; + } else { + break; + } + } + + targetAccessor.setFrame(targetFrame); + targetAccessor.setSubFrame(ticks + sourceAccessor.getSubFrame()); + } + + protected static class PBRTextureAtlasSpriteInfo extends TextureAtlasSpriteInfo { + protected final PBRType pbrType; + + public PBRTextureAtlasSpriteInfo(ResourceLocation name, int width, int height, AnimationMetadataSection metadata, PBRType pbrType) { + super(name, width, height, metadata); + this.pbrType = pbrType; + } + } + + public static class PBRTextureAtlasSprite extends TextureAtlasSprite implements CustomMipmapGenerator.Provider { + // This feels super janky + protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, NativeImage nativeImage, TextureMap texMap, int miplevel) { + super(info.name().toString()); + super.initSprite(atlasWidth, atlasHeight, x, y, false); + super.loadSprite(getMipmapGenerator(info, atlasWidth, atlasHeight).generateMipLevels(nativeImage, miplevel), animationMetaDataSection, (float)((TextureMapAccessor) texMap).getAnisotropicFiltering() > 1.0F); + } + + @Override + public CustomMipmapGenerator getMipmapGenerator(TextureAtlasSpriteInfo info, int atlasWidth, int atlasHeight) { + if (info instanceof PBRTextureAtlasSpriteInfo pbrInfo) { + final PBRType pbrType = pbrInfo.pbrType; + final TextureFormat format = TextureFormatLoader.getFormat(); + if (format != null) { + final CustomMipmapGenerator generator = format.getMipmapGenerator(pbrType); + if (generator != null) { + return generator; + } + } + } + return LINEAR_MIPMAP_GENERATOR; + } + } + + private Pair getFrameSize(int i, int j, AnimationMetadataSection animationMetadataSection) { + final Pair pair = this.calculateFrameSize(i, j, animationMetadataSection); + final int k = pair.getLeft(); + final int l = pair.getRight(); + if (isDivisionInteger(i, k) && isDivisionInteger(j, l)) { + return pair; + } else { + throw new IllegalArgumentException(String.format("Image size %s,%s is not multiply of frame size %s,%s", i, j, k, l)); + } + } + + private Pair calculateFrameSize(int i, int j, AnimationMetadataSection animationMetadataSection) { + if (animationMetadataSection.getFrameWidth() != -1) { + return animationMetadataSection.getFrameHeight() != -1 ? Pair.of(animationMetadataSection.getFrameWidth(), animationMetadataSection.getFrameHeight()) : Pair.of(animationMetadataSection.getFrameWidth(), j); + } else if (animationMetadataSection.getFrameHeight() != -1) { + return Pair.of(i, animationMetadataSection.getFrameHeight()); + } else { + int k = Math.min(i, j); + return Pair.of(k, k); + } + } + + private static boolean isDivisionInteger(int i, int j) { + return i / j * j == i; + } + +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoader.java new file mode 100644 index 000000000..a7c4977f9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoader.java @@ -0,0 +1,23 @@ +package net.coderbot.iris.texture.pbr.loader; + +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.resources.IResourceManager; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +public interface PBRTextureLoader { + /** + * This method must not modify global GL state except the texture binding for {@link GL11.GL_TEXTURE_2D}. + * + * @param texture The base texture. + * @param resourceManager The resource manager. + * @param pbrTextureConsumer The consumer that accepts resulting PBR textures. + */ + void load(T texture, IResourceManager resourceManager, PBRTextureConsumer pbrTextureConsumer); + + interface PBRTextureConsumer { + void acceptNormalTexture(@NotNull AbstractTexture texture); + + void acceptSpecularTexture(@NotNull AbstractTexture texture); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoaderRegistry.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoaderRegistry.java new file mode 100644 index 000000000..48627aceb --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/PBRTextureLoaderRegistry.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.texture.pbr.loader; + +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.SimpleTexture; +import net.minecraft.client.renderer.texture.TextureMap; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class PBRTextureLoaderRegistry { + public static final PBRTextureLoaderRegistry INSTANCE = new PBRTextureLoaderRegistry(); + + static { + INSTANCE.register(SimpleTexture.class, new SimplePBRLoader()); + INSTANCE.register(TextureMap.class, new AtlasPBRLoader()); + } + + private final Map, PBRTextureLoader> loaderMap = new HashMap<>(); + + public void register(Class clazz, PBRTextureLoader loader) { + loaderMap.put(clazz, loader); + } + + @SuppressWarnings("unchecked") + @Nullable + public PBRTextureLoader getLoader(Class clazz) { + return (PBRTextureLoader) loaderMap.get(clazz); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java new file mode 100644 index 000000000..fed180dcc --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.texture.pbr.loader; + +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.SimpleTextureAccessor; +import net.coderbot.iris.texture.pbr.PBRType; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.SimpleTexture; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class SimplePBRLoader implements PBRTextureLoader { + @Override + public void load(SimpleTexture texture, IResourceManager resourceManager, PBRTextureConsumer pbrTextureConsumer) { + ResourceLocation location = ((SimpleTextureAccessor) texture).getLocation(); + + AbstractTexture normalTexture = createPBRTexture(location, resourceManager, PBRType.NORMAL); + AbstractTexture specularTexture = createPBRTexture(location, resourceManager, PBRType.SPECULAR); + + if (normalTexture != null) { + pbrTextureConsumer.acceptNormalTexture(normalTexture); + } + if (specularTexture != null) { + pbrTextureConsumer.acceptSpecularTexture(specularTexture); + } + } + + @Nullable + protected AbstractTexture createPBRTexture(ResourceLocation imageLocation, IResourceManager resourceManager, PBRType pbrType) { + ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation); + + SimpleTexture pbrTexture = new SimpleTexture(pbrImageLocation); + try { + pbrTexture.loadTexture(resourceManager); + } catch (IOException e) { + return null; + } + + return pbrTexture; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/TextureAtlasSpriteInfo.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/TextureAtlasSpriteInfo.java new file mode 100644 index 000000000..28f0a781f --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/TextureAtlasSpriteInfo.java @@ -0,0 +1,30 @@ +package net.coderbot.iris.texture.pbr.loader; + +import net.minecraft.client.resources.data.AnimationMetadataSection; +import net.minecraft.util.ResourceLocation; + +public class TextureAtlasSpriteInfo { + private final ResourceLocation name; + private final int width; + private final int height; + private final AnimationMetadataSection metadata; + + public TextureAtlasSpriteInfo(ResourceLocation arg, int i, int j, AnimationMetadataSection arg2) { + this.name = arg; + this.width = i; + this.height = j; + this.metadata = arg2; + } + + public ResourceLocation name() { + return this.name; + } + + public int width() { + return this.width; + } + + public int height() { + return this.height; + } +} diff --git a/src/main/java/net/coderbot/iris/texture/util/ImageManipulationUtil.java b/src/main/java/net/coderbot/iris/texture/util/ImageManipulationUtil.java new file mode 100644 index 000000000..365c56355 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/util/ImageManipulationUtil.java @@ -0,0 +1,121 @@ +package net.coderbot.iris.texture.util; + +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; + +public class ImageManipulationUtil { + public static NativeImage scaleNearestNeighbor(NativeImage image, int newWidth, int newHeight) { + NativeImage scaled = new NativeImage(image.getFormat(), newWidth, newHeight, false); + float xScale = (float) newWidth / image.getWidth(); + float yScale = (float) newHeight / image.getHeight(); + for (int y = 0; y < newHeight; ++y) { + for (int x = 0; x < newWidth; ++x) { + float unscaledX = (x + 0.5f) / xScale; + float unscaledY = (y + 0.5f) / yScale; + scaled.setPixelRGBA(x, y, image.getPixelRGBA((int) unscaledX, (int) unscaledY)); + } + } + return scaled; + } + + public static NativeImage scaleBilinear(NativeImage image, int newWidth, int newHeight) { + NativeImage scaled = new NativeImage(image.getFormat(), newWidth, newHeight, false); + float xScale = (float) newWidth / image.getWidth(); + float yScale = (float) newHeight / image.getHeight(); + for (int y = 0; y < newHeight; ++y) { + for (int x = 0; x < newWidth; ++x) { + float unscaledX = (x + 0.5f) / xScale; + float unscaledY = (y + 0.5f) / yScale; + + int x1 = Math.round(unscaledX); + int y1 = Math.round(unscaledY); + int x0 = x1 - 1; + int y0 = y1 - 1; + + boolean x0valid = true; + boolean y0valid = true; + boolean x1valid = true; + boolean y1valid = true; + if (x0 < 0) { + x0valid = false; + } + if (y0 < 0) { + y0valid = false; + } + if (x1 >= image.getWidth()) { + x1valid = false; + } + if (y1 >= image.getHeight()) { + y1valid = false; + } + + int finalColor = 0; + if (x0valid & y0valid & x1valid & y1valid) { + float leftWeight = (x1 + 0.5f) - unscaledX; + float rightWeight = unscaledX - (x0 + 0.5f); + float topWeight = (y1 + 0.5f) - unscaledY; + float bottomWeight = unscaledY - (y0 + 0.5f); + + float weightTL = leftWeight * topWeight; + float weightTR = rightWeight * topWeight; + float weightBL = leftWeight * bottomWeight; + float weightBR = rightWeight * bottomWeight; + + int colorTL = image.getPixelRGBA(x0, y0); + int colorTR = image.getPixelRGBA(x1, y0); + int colorBL = image.getPixelRGBA(x0, y1); + int colorBR = image.getPixelRGBA(x1, y1); + + finalColor = blendColor(colorTL, colorTR, colorBL, colorBR, weightTL, weightTR, weightBL, weightBR); + } else if (x0valid & x1valid) { + float leftWeight = (x1 + 0.5f) - unscaledX; + float rightWeight = unscaledX - (x0 + 0.5f); + + int validY = y0valid ? y0 : y1; + int colorLeft = image.getPixelRGBA(x0, validY); + int colorRight = image.getPixelRGBA(x1, validY); + + finalColor = blendColor(colorLeft, colorRight, leftWeight, rightWeight); + } else if (y0valid & y1valid) { + float topWeight = (y1 + 0.5f) - unscaledY; + float bottomWeight = unscaledY - (y0 + 0.5f); + + int validX = x0valid ? x0 : x1; + int colorTop = image.getPixelRGBA(validX, y0); + int colorBottom = image.getPixelRGBA(validX, y1); + + finalColor = blendColor(colorTop, colorBottom, topWeight, bottomWeight); + } else { + finalColor = image.getPixelRGBA(x0valid ? x0 : x1, y0valid ? y0 : y1); + } + scaled.setPixelRGBA(x, y, finalColor); + } + } + return scaled; + } + + private static int blendColor(int c0, int c1, int c2, int c3, float w0, float w1, float w2, float w3) { + return NativeImage.combine( + blendChannel(NativeImage.getA(c0), NativeImage.getA(c1), NativeImage.getA(c2), NativeImage.getA(c3), w0, w1, w2, w3), + blendChannel(NativeImage.getB(c0), NativeImage.getB(c1), NativeImage.getB(c2), NativeImage.getB(c3), w0, w1, w2, w3), + blendChannel(NativeImage.getG(c0), NativeImage.getG(c1), NativeImage.getG(c2), NativeImage.getG(c3), w0, w1, w2, w3), + blendChannel(NativeImage.getR(c0), NativeImage.getR(c1), NativeImage.getR(c2), NativeImage.getR(c3), w0, w1, w2, w3) + ); + } + + private static int blendChannel(int v0, int v1, int v2, int v3, float w0, float w1, float w2, float w3) { + return Math.round(v0 * w0 + v1 * w1 + v2 * w2 + v3 * w3); + } + + private static int blendColor(int c0, int c1, float w0, float w1) { + return NativeImage.combine( + blendChannel(NativeImage.getA(c0), NativeImage.getA(c1), w0, w1), + blendChannel(NativeImage.getB(c0), NativeImage.getB(c1), w0, w1), + blendChannel(NativeImage.getG(c0), NativeImage.getG(c1), w0, w1), + blendChannel(NativeImage.getR(c0), NativeImage.getR(c1), w0, w1) + ); + } + + private static int blendChannel(int v0, int v1, float w0, float w1) { + return Math.round(v0 * w0 + v1 * w1); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/util/TextureExporter.java b/src/main/java/net/coderbot/iris/texture/util/TextureExporter.java new file mode 100644 index 000000000..4a89d9583 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/util/TextureExporter.java @@ -0,0 +1,33 @@ +package net.coderbot.iris.texture.util; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.compat.mojang.NativeImage; +import net.minecraft.client.Minecraft; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.opengl.GL11; + +import java.io.File; + +public class TextureExporter { + public static void exportTextures(String directory, String filename, int textureId, int mipLevel, int width, int height) { + String extension = FilenameUtils.getExtension(filename); + String baseName = filename.substring(0, filename.length() - extension.length() - 1); + for (int level = 0; level <= mipLevel; ++level) { + exportTexture(directory, baseName + "_" + level + "." + extension, textureId, level, width >> level, height >> level); + } + } + + public static void exportTexture(String directory, String filename, int textureId, int level, int width, int height) { + NativeImage nativeImage = new NativeImage(width, height, false); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textureId); + nativeImage.downloadTexture(level, false); + + File dir = new File(Minecraft.getMinecraft().mcDataDir, directory); + dir.mkdirs(); + File file = new File(dir, filename); + + try { + nativeImage.writeToFile(file); + } catch (Exception ignored) {} + } +} diff --git a/src/main/java/net/coderbot/iris/texture/util/TextureManipulationUtil.java b/src/main/java/net/coderbot/iris/texture/util/TextureManipulationUtil.java new file mode 100644 index 000000000..9b5ab5bf4 --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/util/TextureManipulationUtil.java @@ -0,0 +1,59 @@ +package net.coderbot.iris.texture.util; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class TextureManipulationUtil { + private static int colorFillFBO = -1; + + public static void fillWithColor(int textureId, int maxLevel, int rgba) { + if (colorFillFBO == -1) { + colorFillFBO = OpenGlHelper.func_153165_e/*glGenFramebuffers*/(); + } + + int previousFramebufferId = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + // TODO: allocations + FloatBuffer previousClearColorBuffer = BufferUtils.createFloatBuffer(4); +// float[] previousClearColor = new float[4]; + IrisRenderSystem.getFloatv(GL11.GL_COLOR_CLEAR_VALUE, previousClearColorBuffer); + int previousTextureId = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + IntBuffer previousViewportBuffer = BufferUtils.createIntBuffer(4); +// int[] previousViewport = new int[4]; + IrisRenderSystem.getIntegerv(GL11.GL_VIEWPORT, previousViewportBuffer); + + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, colorFillFBO); + GL11.glClearColor( + (rgba >> 24 & 0xFF) / 255.0f, + (rgba >> 16 & 0xFF) / 255.0f, + (rgba >> 8 & 0xFF) / 255.0f, + (rgba & 0xFF) / 255.0f + ); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textureId); + for (int level = 0; level <= maxLevel; ++level) { + int width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_WIDTH); + int height = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_HEIGHT); + GL11.glViewport(0, 0, width, height); + GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, textureId, level); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); + if (Minecraft.isRunningOnMac) { + GL11.glGetError(); + } + + + GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, 0, level); + } + + OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, previousFramebufferId); + GL11.glClearColor(previousClearColorBuffer.get(0), previousClearColorBuffer.get(1), previousClearColorBuffer.get(2), previousClearColorBuffer.get(3)); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, previousTextureId); + GL11.glViewport(previousViewportBuffer.get(0), previousViewportBuffer.get(1), previousViewportBuffer.get(2), previousViewportBuffer.get(3)); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/CameraUniforms.java b/src/main/java/net/coderbot/iris/uniforms/CameraUniforms.java new file mode 100644 index 000000000..a7c7f5062 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/CameraUniforms.java @@ -0,0 +1,110 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.rendering.RenderingState; +import lombok.Getter; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.minecraft.client.Minecraft; +import org.joml.Vector3d; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.ONCE; +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; + +/** + * @see Uniforms: Camera + */ +public class CameraUniforms { + private static final Minecraft client = Minecraft.getMinecraft(); + + private CameraUniforms() { + } + + public static void addCameraUniforms(UniformHolder uniforms, FrameUpdateNotifier notifier) { + final CameraPositionTracker tracker = new CameraPositionTracker(notifier); + + uniforms + .uniform1f(ONCE, "near", () -> 0.05) + .uniform1f(PER_FRAME, "far", CameraUniforms::getRenderDistanceInBlocks) + .uniform3d(PER_FRAME, "cameraPosition", tracker::getCurrentCameraPosition) + .uniform1f(PER_FRAME, "eyeAltitude", tracker::getCurrentCameraPositionY) + .uniform3d(PER_FRAME, "previousCameraPosition", tracker::getPreviousCameraPosition); + } + + private static int getRenderDistanceInBlocks() { + // TODO: Should we ask the game renderer for this? + return client.gameSettings.renderDistanceChunks * 16; + } + + public static Vector3d getUnshiftedCameraPosition() { + return RenderingState.INSTANCE.getCameraPosition(); + } + + static class CameraPositionTracker { + /** + * Value range of cameraPosition. We want this to be small enough that precision is maintained when we convert + * from a double to a float, but big enough that shifts happen infrequently, since each shift corresponds with + * a noticeable change in shader animations and similar. 1000024 is the number used by Optifine for walking (however this is too much, so we choose 30000), + * with an extra 1024 check for if the user has teleported between camera positions. + */ + private static final double WALK_RANGE = 30000; + private static final double TP_RANGE = 1000; + + @Getter + private Vector3d previousCameraPosition = new Vector3d(); + @Getter + private Vector3d currentCameraPosition = new Vector3d(); + private final Vector3d shift = new Vector3d(); + + CameraPositionTracker(FrameUpdateNotifier notifier) { + notifier.addListener(this::update); + } + + private void update() { + previousCameraPosition = currentCameraPosition; + currentCameraPosition = getUnshiftedCameraPosition().add(shift); + + updateShift(); + } + + /** + * Updates our shift values to try to keep |x| < 30000 and |z| < 30000, to maintain precision with cameraPosition. + * Since our actual range is 60000x60000, this means that we won't excessively move back and forth when moving + * around a chunk border. + */ + private void updateShift() { + final double dX = getShift(currentCameraPosition.x, previousCameraPosition.x); + final double dZ = getShift(currentCameraPosition.z, previousCameraPosition.z); + + if (dX != 0.0 || dZ != 0.0) { + applyShift(dX, dZ); + } + } + + private static double getShift(double value, double prevValue) { + if (Math.abs(value) > WALK_RANGE || Math.abs(value - prevValue) > TP_RANGE) { + // Only shift by increments of WALK_RANGE - this is required for some packs (like SEUS PTGI) to work properly + return -(value - (value % WALK_RANGE)); + } else { + return 0.0; + } + } + + /** + * Shifts all current and future positions by the given amount. This is done in such a way that the difference + * between cameraPosition and previousCameraPosition remains the same, since they are completely arbitrary + * to the shader for the most part. + */ + private void applyShift(double dX, double dZ) { + shift.x += dX; + currentCameraPosition.x += dX; + previousCameraPosition.x += dX; + + shift.z += dZ; + currentCameraPosition.z += dZ; + previousCameraPosition.z += dZ; + } + + public double getCurrentCameraPositionY() { + return currentCameraPosition.y; + } + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/CapturedRenderingState.java b/src/main/java/net/coderbot/iris/uniforms/CapturedRenderingState.java new file mode 100644 index 000000000..dd9699e3b --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/CapturedRenderingState.java @@ -0,0 +1,49 @@ +package net.coderbot.iris.uniforms; + +import lombok.Getter; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; + +public class CapturedRenderingState { + public static final CapturedRenderingState INSTANCE = new CapturedRenderingState(); + + @Getter + private float tickDelta; + @Getter + private int currentRenderedBlockEntity; + private Runnable blockEntityIdListener = null; + + @Getter + private int currentRenderedEntity = -1; + private Runnable entityIdListener = null; + + private CapturedRenderingState() { + } + + public void setTickDelta(float tickDelta) { + this.tickDelta = tickDelta; + } + + public void setCurrentBlockEntity(int entity) { + this.currentRenderedBlockEntity = entity; + + if (this.blockEntityIdListener != null) { + this.blockEntityIdListener.run(); + } + } + + public void setCurrentEntity(int entity) { + this.currentRenderedEntity = entity; + + if (this.entityIdListener != null) { + this.entityIdListener.run(); + } + } + + public ValueUpdateNotifier getEntityIdNotifier() { + return listener -> this.entityIdListener = listener; + } + + public ValueUpdateNotifier getBlockEntityIdNotifier() { + return listener -> this.blockEntityIdListener = listener; + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/CelestialUniforms.java b/src/main/java/net/coderbot/iris/uniforms/CelestialUniforms.java new file mode 100644 index 000000000..a51bcb79a --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/CelestialUniforms.java @@ -0,0 +1,134 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.compat.mojang.Constants; +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; +import org.joml.Matrix4f; +import org.joml.Vector4f; + +import java.util.Objects; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; + +/** + * @see Uniforms: Celestial bodies + */ +public final class CelestialUniforms { + + private final float sunPathRotation; + + public CelestialUniforms(float sunPathRotation) { + this.sunPathRotation = sunPathRotation; + } + + public void addCelestialUniforms(UniformHolder uniforms) { + uniforms + .uniform1f(PER_FRAME, "sunAngle", CelestialUniforms::getSunAngle) + .uniformTruncated3f(PER_FRAME, "sunPosition", this::getSunPosition) + .uniformTruncated3f(PER_FRAME, "moonPosition", this::getMoonPosition) + .uniform1f(PER_FRAME, "shadowAngle", CelestialUniforms::getShadowAngle) + .uniformTruncated3f(PER_FRAME, "shadowLightPosition", this::getShadowLightPosition) + .uniformTruncated3f(PER_FRAME, "upPosition", CelestialUniforms::getUpPosition); + } + + public static float getSunAngle() { + final float skyAngle = getSkyAngle(); + + if (skyAngle < 0.75F) { + return skyAngle + 0.25F; + } else { + return skyAngle - 0.75F; + } + } + + private static float getShadowAngle() { + float shadowAngle = getSunAngle(); + + if (!isDay()) { + shadowAngle -= 0.5F; + } + + return shadowAngle; + } + + private Vector4f getSunPosition() { + return getCelestialPosition(100.0F); + } + + private Vector4f getMoonPosition() { + return getCelestialPosition(-100.0F); + } + + public Vector4f getShadowLightPosition() { + return isDay() ? getSunPosition() : getMoonPosition(); + } + + public Vector4f getShadowLightPositionInWorldSpace() { + return isDay() ? getCelestialPositionInWorldSpace(100.0F) : getCelestialPositionInWorldSpace(-100.0F); + } + + private Vector4f getCelestialPositionInWorldSpace(float y) { + final Vector4f position = new Vector4f(0.0F, y, 0.0F, 0.0F); + + // TODO: Deduplicate / remove this function. + final Matrix4f celestial = new Matrix4f().identity(); + + // This is the same transformation applied by renderSky, however, it's been moved to here. + // This is because we need the result of it before it's actually performed in vanilla. + celestial.rotateY(-90.F * Constants.DEGREES_TO_RADIANS); + celestial.rotateZ(sunPathRotation * Constants.DEGREES_TO_RADIANS); + celestial.rotateX(getSkyAngle() * 360.0F * Constants.DEGREES_TO_RADIANS); + + celestial.transform(position); + + return position; + } + + private Vector4f getCelestialPosition(float y) { + Vector4f position = new Vector4f(0.0F, y, 0.0F, 0.0F); + + final Matrix4f celestial = new Matrix4f(RenderingState.INSTANCE.getModelViewMatrix()); + // This is the same transformation applied by renderSky, however, it's been moved to here. + // This is because we need the result of it before it's actually performed in vanilla. + celestial.rotateY(-90.F * Constants.DEGREES_TO_RADIANS); + celestial.rotateZ(sunPathRotation * Constants.DEGREES_TO_RADIANS); + celestial.rotateX(getSkyAngle() * 360.0F * Constants.DEGREES_TO_RADIANS); + + position = celestial.transform(position); + + return position; + } + + private static Vector4f getUpPosition() { + Vector4f upVector = new Vector4f(0.0F, 100.0F, 0.0F, 0.0F); + + // Get the current model view matrix, since that is the basis of the celestial model view matrix + final Matrix4f preCelestial = new Matrix4f(RenderingState.INSTANCE.getModelViewMatrix()); + + // Apply the fixed -90.0F degrees rotation to mirror the same transformation in renderSky. + // But, notably, skip the rotation by the skyAngle. + preCelestial.rotateY(-90.F * Constants.DEGREES_TO_RADIANS); + + // Use this matrix to transform the vector. + upVector = preCelestial.transform(upVector); + + return upVector; + } + + public static boolean isDay() { + // Determine whether it is day or night based on the sky angle. + // + // World#isDay appears to do some nontrivial calculations that appear to not entirely work for us here. + return getSunAngle() <= 0.5; + } + + private static WorldClient getWorld() { + return Objects.requireNonNull(Minecraft.getMinecraft().theWorld); + } + + private static float getSkyAngle() { + return getWorld().getCelestialAngle(CapturedRenderingState.INSTANCE.getTickDelta()); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/CommonUniforms.java b/src/main/java/net/coderbot/iris/uniforms/CommonUniforms.java new file mode 100644 index 000000000..d9fdd25d9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/CommonUniforms.java @@ -0,0 +1,253 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.BlendState; +import com.gtnewhorizons.angelica.mixins.early.shaders.accessors.EntityRendererAccessor; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.gl.uniform.DynamicUniformHolder; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.layer.GbufferPrograms; +import net.coderbot.iris.shaderpack.IdMap; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.texture.TextureInfoCache; +import net.coderbot.iris.texture.TextureTracker; +import net.coderbot.iris.uniforms.transforms.SmoothedFloat; +import net.coderbot.iris.uniforms.transforms.SmoothedVec2f; +import net.minecraft.block.material.Material; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.potion.Potion; +import net.minecraft.potion.PotionEffect; +import net.minecraft.util.Vec3; +import org.joml.Math; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3d; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.ONCE; +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_TICK; + +public final class CommonUniforms { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final Vector2i ZERO_VECTOR_2i = new Vector2i(); + private static final Vector4i ZERO_VECTOR_4i = new Vector4i(0, 0, 0, 0); + private static final Vector3d ZERO_VECTOR_3d = new Vector3d(); + + private CommonUniforms() { + // no construction allowed + } + + // Needs to use a LocationalUniformHolder as we need it for the common uniforms + public static void addCommonUniforms(DynamicUniformHolder uniforms, IdMap idMap, PackDirectives directives, FrameUpdateNotifier updateNotifier) { + CameraUniforms.addCameraUniforms(uniforms, updateNotifier); + ViewportUniforms.addViewportUniforms(uniforms); + WorldTimeUniforms.addWorldTimeUniforms(uniforms); + SystemTimeUniforms.addSystemTimeUniforms(uniforms); + new CelestialUniforms(directives.getSunPathRotation()).addCelestialUniforms(uniforms); + IdMapUniforms.addIdMapUniforms(updateNotifier, uniforms, idMap, directives.isOldHandLight()); + IrisExclusiveUniforms.addIrisExclusiveUniforms(uniforms); + MatrixUniforms.addMatrixUniforms(uniforms, directives); + HardcodedCustomUniforms.addHardcodedCustomUniforms(uniforms, updateNotifier); + FogUniforms.addFogUniforms(uniforms); + + // TODO: OptiFine doesn't think that atlasSize is a "dynamic" uniform, + // but we do. How will custom uniforms depending on atlasSize work? + uniforms.uniform2i("atlasSize", () -> { + final int glId = GLStateManager.getTextures()[0].binding; + + final AbstractTexture texture = TextureTracker.INSTANCE.getTexture(glId); + if (texture instanceof TextureMap) { + final TextureInfoCache.TextureInfo info = TextureInfoCache.INSTANCE.getInfo(glId); + return new Vector2i(info.getWidth(), info.getHeight()); + } + + return ZERO_VECTOR_2i; + }, StateUpdateNotifiers.bindTextureNotifier); + + uniforms.uniform2i("gtextureSize", () -> { + final int glId = GLStateManager.getTextures()[0].binding; + + final TextureInfoCache.TextureInfo info = TextureInfoCache.INSTANCE.getInfo(glId); + return new Vector2i(info.getWidth(), info.getHeight()); + + }, StateUpdateNotifiers.bindTextureNotifier); + + uniforms.uniform4i("blendFunc", () -> { + final BlendState blend = GLStateManager.getBlendState(); + if(blend.mode.isEnabled()) { + return new Vector4i(blend.srcRgb, blend.dstRgb, blend.srcAlpha, blend.dstAlpha); + } + return ZERO_VECTOR_4i; + }, StateUpdateNotifiers.blendFuncNotifier); + + uniforms.uniform1i("renderStage", () -> GbufferPrograms.getCurrentPhase().ordinal(), StateUpdateNotifiers.phaseChangeNotifier); + + CommonUniforms.generalCommonUniforms(uniforms, updateNotifier, directives); + } + + public static void generalCommonUniforms(UniformHolder uniforms, FrameUpdateNotifier updateNotifier, PackDirectives directives) { + ExternallyManagedUniforms.addExternallyManagedUniforms116(uniforms); + + final SmoothedVec2f eyeBrightnessSmooth = new SmoothedVec2f(directives.getEyeBrightnessHalfLife(), directives.getEyeBrightnessHalfLife(), CommonUniforms::getEyeBrightness, updateNotifier); + + uniforms + .uniform1b(PER_FRAME, "hideGUI", () -> client.gameSettings.hideGUI) + .uniform1i(PER_FRAME, "isEyeInWater", CommonUniforms::isEyeInWater) + .uniform1f(PER_FRAME, "blindness", CommonUniforms::getBlindness) + .uniform1f(PER_FRAME, "nightVision", CommonUniforms::getNightVision) + .uniform1b(PER_FRAME, "is_sneaking", CommonUniforms::isSneaking) + .uniform1b(PER_FRAME, "is_sprinting", CommonUniforms::isSprinting) + .uniform1b(PER_FRAME, "is_hurt", CommonUniforms::isHurt) + .uniform1b(PER_FRAME, "is_invisible", CommonUniforms::isInvisible) + .uniform1b(PER_FRAME, "is_burning", CommonUniforms::isBurning) + .uniform1b(PER_FRAME, "is_on_ground", CommonUniforms::isOnGround) + // TODO: Do we need to clamp this to avoid fullbright breaking shaders? Or should shaders be able to detect + // that the player is trying to turn on fullbright? + .uniform1f(PER_FRAME, "screenBrightness", () -> client.gameSettings.gammaSetting) + // just a dummy value for shaders where entityColor isn't supplied through a vertex attribute (and thus is + // not available) - suppresses warnings. See AttributeShaderTransformer for the actual entityColor code. + .uniform4f(ONCE, "entityColor", Vector4f::new) + .uniform1f(PER_TICK, "playerMood", CommonUniforms::getPlayerMood) + .uniform2i(PER_FRAME, "eyeBrightness", CommonUniforms::getEyeBrightness) + .uniform2i(PER_FRAME, "eyeBrightnessSmooth", () -> { + final Vector2f smoothed = eyeBrightnessSmooth.get(); + return new Vector2i((int) smoothed.x(),(int) smoothed.y()); + }) + .uniform1f(PER_TICK, "rainStrength", CommonUniforms::getRainStrength) + .uniform1f(PER_TICK, "wetness", new SmoothedFloat(directives.getWetnessHalfLife(), directives.getDrynessHalfLife(), CommonUniforms::getRainStrength, updateNotifier)) + .uniform3d(PER_FRAME, "skyColor", CommonUniforms::getSkyColor) + .uniform3d(PER_FRAME, "fogColor", GLStateManager::getFogColor); + } + + private static boolean isOnGround() { + return client.thePlayer != null && client.thePlayer.onGround; + } + + private static boolean isHurt() { + // Do not use isHurt, that's not what we want! + return (client.thePlayer != null && client.thePlayer.hurtTime > 0); + } + + private static boolean isInvisible() { + return (client.thePlayer != null && client.thePlayer.isInvisible()); + } + + private static boolean isBurning() { + return client.thePlayer != null && client.thePlayer.fire > 0 && !client.thePlayer.isImmuneToFire(); + } + + private static boolean isSneaking() { + return (client.thePlayer != null && client.thePlayer.isSneaking()); + } + + private static boolean isSprinting() { + return (client.thePlayer != null && client.thePlayer.isSprinting()); + } + + private static Vector3d getSkyColor() { + if (client.theWorld == null || client.renderViewEntity == null) { + return ZERO_VECTOR_3d; + } + final Vec3 skyColor = client.theWorld.getSkyColor(client.renderViewEntity, CapturedRenderingState.INSTANCE.getTickDelta()); + return new Vector3d(skyColor.xCoord, skyColor.yCoord, skyColor.zCoord); + } + + static float getBlindness() { + final EntityLivingBase cameraEntity = client.renderViewEntity; + + if (cameraEntity instanceof EntityLiving livingEntity && livingEntity.isPotionActive(Potion.blindness)) { + final PotionEffect blindness = livingEntity.getActivePotionEffect(Potion.blindness); + + if (blindness != null) { + // Guessing that this is what OF uses, based on how vanilla calculates the fog value in BackgroundRenderer + // TODO: Add this to ShaderDoc + return Math.clamp(0.0F, 1.0F, blindness.getDuration() / 20.0F); + } + } + + return 0.0F; + } + + private static float getPlayerMood() { + // TODO: What should this be? + return 0.0F; +// if (!(client.cameraEntity instanceof LocalPlayer)) { +// return 0.0F; +// } +// +// // This should always be 0 to 1 anyways but just making sure +// return Math.clamp(0.0F, 1.0F, ((LocalPlayer) client.cameraEntity).getCurrentMood()); + } + + static float getRainStrength() { + if (client.theWorld == null) { + return 0f; + } + + // Note: Ensure this is in the range of 0 to 1 - some custom servers send out of range values. + return Math.clamp(0.0F, 1.0F, client.theWorld.getRainStrength(CapturedRenderingState.INSTANCE.getTickDelta())); + + } + + private static Vector2i getEyeBrightness() { + if (client.renderViewEntity == null || client.theWorld == null) { + return ZERO_VECTOR_2i; + } + // This is what ShadersMod did in 1.7.10 + final int eyeBrightness = client.renderViewEntity.getBrightnessForRender(CapturedRenderingState.INSTANCE.getTickDelta()); + return new Vector2i((eyeBrightness & 0xffff), (eyeBrightness >> 16)); + +// Vec3 feet = client.cameraEntity.position(); +// Vec3 eyes = new Vec3(feet.x, client.cameraEntity.getEyeY(), feet.z); +// BlockPos eyeBlockPos = new BlockPos(eyes); +// +// int blockLight = client.level.getBrightness(LightLayer.BLOCK, eyeBlockPos); +// int skyLight = client.level.getBrightness(LightLayer.SKY, eyeBlockPos); +// +// return new Vector2i(blockLight * 16, skyLight * 16); + } + + private static float getNightVision() { + Entity cameraEntity = client.renderViewEntity; + + if (cameraEntity instanceof EntityPlayer entityPlayer) { + if (!entityPlayer.isPotionActive(Potion.nightVision)) { + return 0.0F; + } + float nightVisionStrength = ((EntityRendererAccessor)client.entityRenderer).invokeGetNightVisionBrightness(entityPlayer, CapturedRenderingState.INSTANCE.getTickDelta()); + + try { + if (nightVisionStrength > 0) { + // Just protecting against potential weird mod behavior + return Math.clamp(0.0F, 1.0F, nightVisionStrength); + } + } catch (NullPointerException e) { + return 0.0F; + } + } + + return 0.0F; + } + + static int isEyeInWater() { + if (client.gameSettings.thirdPersonView == 0 && !client.renderViewEntity.isPlayerSleeping()) { + if (client.thePlayer.isInsideOfMaterial(Material.water)) + return 1; + else if (client.thePlayer.isInsideOfMaterial(Material.lava)) + return 2; + } + return 0; + } + + static { + GbufferPrograms.init(); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/ExternallyManagedUniforms.java b/src/main/java/net/coderbot/iris/uniforms/ExternallyManagedUniforms.java new file mode 100644 index 000000000..c4976a447 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/ExternallyManagedUniforms.java @@ -0,0 +1,65 @@ +package net.coderbot.iris.uniforms; + +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformType; + +public class ExternallyManagedUniforms { + private ExternallyManagedUniforms() { + // no construction allowed + } + + public static void addExternallyManagedUniforms(UniformHolder uniformHolder) { + addMat4(uniformHolder, "iris_ModelViewMatrix"); + addMat4(uniformHolder, "u_ModelViewProjectionMatrix"); + addMat4(uniformHolder, "iris_NormalMatrix"); + // Exclusive to pre-1.19 + addFloat(uniformHolder, "darknessFactor"); + addFloat(uniformHolder, "darknessLightFactor"); + } + + public static void addExternallyManagedUniforms116(UniformHolder uniformHolder) { + addExternallyManagedUniforms(uniformHolder); + + uniformHolder.externallyManagedUniform("u_ModelScale", UniformType.VEC3); + uniformHolder.externallyManagedUniform("u_TextureScale", UniformType.VEC2); + } + + public static void addExternallyManagedUniforms117(UniformHolder uniformHolder) { + addExternallyManagedUniforms(uniformHolder); + + // Sodium + addFloat(uniformHolder, "iris_FogStart"); + addFloat(uniformHolder, "iris_FogEnd"); + addVec4(uniformHolder, "iris_FogColor"); + addMat4(uniformHolder, "iris_ProjectionMatrix"); + addFloat(uniformHolder, "u_TextureScale"); + addFloat(uniformHolder, "u_ModelScale"); + addFloat(uniformHolder, "iris_ModelOffset"); + uniformHolder.externallyManagedUniform("iris_CameraTranslation", UniformType.VEC3); + + // Vanilla + uniformHolder.externallyManagedUniform("iris_TextureMat", UniformType.MAT4); + uniformHolder.externallyManagedUniform("iris_ModelViewMat", UniformType.MAT4); + uniformHolder.externallyManagedUniform("iris_ProjMat", UniformType.MAT4); + uniformHolder.externallyManagedUniform("iris_ChunkOffset", UniformType.VEC3); + uniformHolder.externallyManagedUniform("iris_ColorModulator", UniformType.VEC4); + uniformHolder.externallyManagedUniform("iris_FogStart", UniformType.FLOAT); + uniformHolder.externallyManagedUniform("iris_FogEnd", UniformType.FLOAT); + uniformHolder.externallyManagedUniform("iris_FogDensity", UniformType.FLOAT); + uniformHolder.externallyManagedUniform("iris_LineWidth", UniformType.FLOAT); + uniformHolder.externallyManagedUniform("iris_ScreenSize", UniformType.VEC2); + uniformHolder.externallyManagedUniform("iris_FogColor", UniformType.VEC4); + } + + private static void addMat4(UniformHolder uniformHolder, String name) { + uniformHolder.externallyManagedUniform(name, UniformType.MAT4); + } + + private static void addVec4(UniformHolder uniformHolder, String name) { + uniformHolder.externallyManagedUniform(name, UniformType.VEC4); + } + + private static void addFloat(UniformHolder uniformHolder, String name) { + uniformHolder.externallyManagedUniform(name, UniformType.FLOAT); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/FogUniforms.java b/src/main/java/net/coderbot/iris/uniforms/FogUniforms.java new file mode 100644 index 000000000..5f690e658 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/FogUniforms.java @@ -0,0 +1,37 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import net.coderbot.iris.gl.state.StateUpdateNotifiers; +import net.coderbot.iris.gl.uniform.DynamicUniformHolder; + +public class FogUniforms { + private FogUniforms() { + // no construction + } + + public static void addFogUniforms(DynamicUniformHolder uniforms) { + uniforms.uniform1i("fogMode", () -> { + if(!GLStateManager.getFogState().mode.isEnabled()) return 0; + + return GLStateManager.getFogState().fogMode; + }, listener -> { + StateUpdateNotifiers.fogToggleNotifier.setListener(listener); + StateUpdateNotifiers.fogModeNotifier.setListener(listener); + }); + + uniforms.uniform1f("fogDensity", () -> GLStateManager.getFogState().density, listener -> { + StateUpdateNotifiers.fogToggleNotifier.setListener(listener); + StateUpdateNotifiers.fogDensityNotifier.setListener(listener); + }); + + uniforms.uniform1f("fogStart", () -> GLStateManager.getFogState().start, listener -> { + StateUpdateNotifiers.fogToggleNotifier.setListener(listener); + StateUpdateNotifiers.fogStartNotifier.setListener(listener); + }); + + uniforms.uniform1f("fogEnd", () -> GLStateManager.getFogState().end, listener -> { + StateUpdateNotifiers.fogToggleNotifier.setListener(listener); + StateUpdateNotifiers.fogEndNotifier.setListener(listener); + }); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/FrameUpdateNotifier.java b/src/main/java/net/coderbot/iris/uniforms/FrameUpdateNotifier.java new file mode 100644 index 000000000..b7745ffce --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/FrameUpdateNotifier.java @@ -0,0 +1,20 @@ +package net.coderbot.iris.uniforms; + +import java.util.ArrayList; +import java.util.List; + +public class FrameUpdateNotifier { + private final List listeners; + + public FrameUpdateNotifier() { + listeners = new ArrayList<>(); + } + + public void addListener(Runnable onNewFrame) { + listeners.add(onNewFrame); + } + + public void onNewFrame() { + listeners.forEach(Runnable::run); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/HardcodedCustomUniforms.java b/src/main/java/net/coderbot/iris/uniforms/HardcodedCustomUniforms.java new file mode 100644 index 000000000..24994aeaf --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/HardcodedCustomUniforms.java @@ -0,0 +1,225 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.uniforms.transforms.SmoothedFloat; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import org.joml.Math; +import org.joml.Vector3d; + +// These expressions are copied directly from BSL and Complementary. + +// TODO: Remove once custom uniforms are actually supported, this is just a temporary thing to get BSL & Complementary +// mostly working under Iris. +public class HardcodedCustomUniforms { + private static final Minecraft client = Minecraft.getMinecraft(); + // TODO: Biome +// private static Biome storedBiome; + + public static void addHardcodedCustomUniforms(UniformHolder holder, FrameUpdateNotifier updateNotifier) { + updateNotifier.addListener(() -> { +// if (Minecraft.getMinecraft().level != null) { +// storedBiome = Minecraft.getMinecraft().level.getBiome(Minecraft.getMinecraft().getCameraEntity().blockPosition()); +// } else { +// storedBiome = null; +// } + }); + + CameraUniforms.CameraPositionTracker tracker = new CameraUniforms.CameraPositionTracker(updateNotifier); + + final SmoothedFloat eyeInCave = new SmoothedFloat(6, 12, HardcodedCustomUniforms::getEyeInCave, updateNotifier); + final SmoothedFloat rainStrengthS = rainStrengthS(updateNotifier, 15, 15); + final SmoothedFloat rainStrengthShining = rainStrengthS(updateNotifier, 10, 11); + final SmoothedFloat rainStrengthS2 = rainStrengthS(updateNotifier, 70, 1); + + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "timeAngle", HardcodedCustomUniforms::getTimeAngle); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "timeBrightness", HardcodedCustomUniforms::getTimeBrightness); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "moonBrightness", HardcodedCustomUniforms::getMoonBrightness); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "shadowFade", HardcodedCustomUniforms::getShadowFade); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "rainStrengthS", rainStrengthS); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "rainStrengthShiningStars", rainStrengthShining); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "rainStrengthS2", rainStrengthS2); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "blindFactor", HardcodedCustomUniforms::getBlindFactor); + // The following uniforms are Complementary specific, used for the biome check and starter/TAA features. + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "isDry", new SmoothedFloat(20, 10, () -> getRawPrecipitation() == 0 ? 1 : 0, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "isRainy", new SmoothedFloat(20, 10, () -> getRawPrecipitation() == 1 ? 1 : 0, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "isSnowy", new SmoothedFloat(20, 10, () -> getRawPrecipitation() == 2 ? 1 : 0, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "isEyeInCave", () -> CommonUniforms.isEyeInWater() == 0 ? eyeInCave.getAsFloat() : 0); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "velocity", () -> getVelocity(tracker)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "starter", getStarter(tracker, updateNotifier)); + // The following uniforms are Project Reimagined specific. + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "frameTimeSmooth", new SmoothedFloat(5, 5, SystemTimeUniforms.TIMER::getLastFrameTime, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "eyeBrightnessM", new SmoothedFloat(5, 5, HardcodedCustomUniforms::getEyeBrightnessM, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "rainFactor", rainStrengthS); + + // The following uniforms are Sildur's specific. + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "inSwamp", new SmoothedFloat(5, 5, () -> { + return 0; +// if (storedBiome == null) { +// return 0; +// } else { +// return storedBiome.getBiomeCategory() == Biome.BiomeCategory.SWAMP ? 1 : 0; +// } + }, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "BiomeTemp", () -> { + return 0; +// if (storedBiome == null) { +// return 0; +// } else { +// return storedBiome.getTemperature(Minecraft.getMinecraft().getCameraEntity().blockPosition()); +// } + }); + + // The following uniforms are specific to Super Duper Vanilla Shaders. + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "day", HardcodedCustomUniforms::getDay); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "night", HardcodedCustomUniforms::getNight); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "dawnDusk", HardcodedCustomUniforms::getDawnDusk); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "shdFade", HardcodedCustomUniforms::getShdFade); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "isPrecipitationRain", new SmoothedFloat(6, 6, () -> (getRawPrecipitation() == 1 && tracker.getCurrentCameraPosition().y < 96.0f) ? 1 : 0, updateNotifier)); + + // The following uniforms are specific to AstralEX, and require an active player. + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "touchmybody", new SmoothedFloat(0f, 0.1f, HardcodedCustomUniforms::getHurtFactor, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "sneakSmooth", new SmoothedFloat(2.0f, 0.9f, HardcodedCustomUniforms::getSneakFactor, updateNotifier)); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "burningSmooth", new SmoothedFloat(1.0f, 2.0f, HardcodedCustomUniforms::getBurnFactor, updateNotifier)); + final SmoothedFloat smoothSpeed = new SmoothedFloat(1.0f, 1.5f, () -> getVelocity(tracker) / SystemTimeUniforms.TIMER.getLastFrameTime(), updateNotifier); + holder.uniform1f(UniformUpdateFrequency.PER_FRAME, "effectStrength", () -> getHyperSpeedStrength(smoothSpeed)); + } + + private static float getHyperSpeedStrength(SmoothedFloat smoothSpeed) { + return (float) (1.0f - Math.exp(-smoothSpeed.getAsFloat() * 0.003906f)); + } + + private static float getBurnFactor() { + final EntityPlayer player = Minecraft.getMinecraft().thePlayer; + return player.fire > 0 && !player.isImmuneToFire() ? 1.0f : 0f; + } + + private static float getSneakFactor() { + return Minecraft.getMinecraft().thePlayer.isSneaking() ? 1.0f : 0f; + } + + private static float getHurtFactor() { + final EntityPlayer player = Minecraft.getMinecraft().thePlayer; + return player.hurtTime > 0 || player.deathTime > 0 ? 0.4f : 0f; + } + + private static float getEyeInCave() { + final Vector3d cameraPosition = RenderingState.INSTANCE.getCameraPosition(); + if (cameraPosition.y < 5.0) { + return 1.0f - getEyeSkyBrightness() / 240F; + } + return 0.0f; + } + + private static float getEyeBrightnessM() { + return getEyeSkyBrightness() / 240F; + } + + private static float getEyeSkyBrightness() { + final int eyeBrightness = client.renderViewEntity.getBrightnessForRender(CapturedRenderingState.INSTANCE.getTickDelta()); + return (eyeBrightness & 0xffff); +// if (client.cameraEntity == null || client.level == null) { +// return 0; +// } + +// Vec3 feet = client.cameraEntity.position(); +// Vec3 eyes = new Vec3(feet.x, client.cameraEntity.getEyeY(), feet.z); +// BlockPos eyeBlockPos = new BlockPos(eyes); +// +// int skyLight = client.level.getBrightness(LightLayer.SKY, eyeBlockPos); +// +// return skyLight * 16; + } + + private static float getVelocity(CameraUniforms.CameraPositionTracker tracker) { + float difX = (float) (tracker.getCurrentCameraPosition().x - tracker.getPreviousCameraPosition().x); + float difY = (float) (tracker.getCurrentCameraPosition().y - tracker.getPreviousCameraPosition().y); + float difZ = (float) (tracker.getCurrentCameraPosition().z - tracker.getPreviousCameraPosition().z); + return Math.sqrt(difX*difX + difY*difY + difZ*difZ); + } + + private static SmoothedFloat getStarter(CameraUniforms.CameraPositionTracker tracker, FrameUpdateNotifier notifier) { + return new SmoothedFloat(20, 20, new SmoothedFloat(0, 31536000, () -> getMoving(tracker), notifier), notifier); + } + + private static float getMoving(CameraUniforms.CameraPositionTracker tracker) { + final float difX = (float) (tracker.getCurrentCameraPosition().x - tracker.getPreviousCameraPosition().x); + final float difY = (float) (tracker.getCurrentCameraPosition().y - tracker.getPreviousCameraPosition().y); + final float difZ = (float) (tracker.getCurrentCameraPosition().z - tracker.getPreviousCameraPosition().z); + final float difSum = Math.abs(difX) + Math.abs(difY) + Math.abs(difZ); + return (difSum > 0.0F && difSum < 1.0F) ? 1 : 0; + } + + private static float getTimeAngle() { + return getWorldDayTime() / 24000F; + } + + private static int getWorldDayTime() { + return (int) (Minecraft.getMinecraft().theWorld.getWorldTime() % 24000L); +// Level level = Minecraft.getMinecraft().theWorld; +// long timeOfDay = level.getDayTime(); +// long dayTime = ((DimensionTypeAccessor) level.dimensionType()).getFixedTime().orElse(timeOfDay % 24000L); + } + + private static float getTimeBrightness() { + return (float) java.lang.Math.max(java.lang.Math.sin(getTimeAngle() * java.lang.Math.PI * 2.0),0.0); + } + + private static float getMoonBrightness() { + return (float) java.lang.Math.max(java.lang.Math.sin(getTimeAngle() * java.lang.Math.PI * (-2.0)),0.0); + } + + private static float getShadowFade() { + return (float) Math.clamp(0.0, 1.0, 1.0 - (java.lang.Math.abs(java.lang.Math.abs(CelestialUniforms.getSunAngle() - 0.5) - 0.25) - 0.23) * 100.0); + } + + private static SmoothedFloat rainStrengthS(FrameUpdateNotifier updateNotifier, float halfLifeUp, float halfLifeDown) { + return new SmoothedFloat(halfLifeUp, halfLifeDown, CommonUniforms::getRainStrength, updateNotifier); + } + + private static float getRawPrecipitation() { + // TODO: Biome +// if (storedBiome == null) { +// return 0; +// } + return 0; +// Biome.Precipitation precipitation = storedBiome.getPrecipitation(); +// return switch (precipitation) { +// case RAIN -> 1; +// case SNOW -> 2; +// default -> 0; +// }; + } + + private static float getBlindFactor() { + float blindFactorSqrt = (float) Math.clamp(0.0, 1.0, CommonUniforms.getBlindness() * 2.0 - 1.0); + return blindFactorSqrt * blindFactorSqrt; + } + + private static float frac(float value) { + return java.lang.Math.abs(value % 1); + } + + private static float getAdjTime() { + return Math.abs(((((WorldTimeUniforms.getWorldDayTime()) / 1000.0f) + 6.0f) % 24.0f) - 12.0f); + } + + private static float getDay() { + return Math.clamp(0.0f, 1.0f, 5.4f - getAdjTime()); + } + + private static float getNight() { + return Math.clamp(0.0f, 1.0f, getAdjTime() - 6.0f); + } + + private static float getDawnDusk() { + return (1.0f - getDay()) - getNight(); + } + + + private static float getShdFade() { + return (float) Math.clamp(0.0, 1.0, 1.0 - (Math.abs(Math.abs(CelestialUniforms.getSunAngle() - 0.5) - 0.25) - 0.225) * 40.0); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/IdMapUniforms.java b/src/main/java/net/coderbot/iris/uniforms/IdMapUniforms.java new file mode 100644 index 000000000..430e20d95 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/IdMapUniforms.java @@ -0,0 +1,146 @@ +package net.coderbot.iris.uniforms; + +import it.unimi.dsi.fastutil.objects.Object2IntFunction; +import com.gtnewhorizons.angelica.compat.mojang.InteractionHand; +import net.coderbot.iris.gl.uniform.DynamicUniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.shaderpack.IdMap; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.irisshaders.iris.api.v0.item.IrisItemLightProvider; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; + +public final class IdMapUniforms { + + private IdMapUniforms() { + } + + public static void addIdMapUniforms(FrameUpdateNotifier notifier, DynamicUniformHolder uniforms, IdMap idMap, boolean isOldHandLight) { + HeldItemSupplier mainHandSupplier = new HeldItemSupplier(InteractionHand.MAIN_HAND, idMap.getItemIdMap(), isOldHandLight); + HeldItemSupplier offHandSupplier = new HeldItemSupplier(InteractionHand.OFF_HAND, idMap.getItemIdMap(), false); + notifier.addListener(mainHandSupplier::update); + notifier.addListener(offHandSupplier::update); + + uniforms + .uniform1i(UniformUpdateFrequency.PER_FRAME, "heldItemId", mainHandSupplier::getIntID) + .uniform1i(UniformUpdateFrequency.PER_FRAME, "heldItemId2", offHandSupplier::getIntID) + .uniform1i(PER_FRAME, "heldBlockLightValue", mainHandSupplier::getLightValue) + .uniform1i(PER_FRAME, "heldBlockLightValue2", offHandSupplier::getLightValue); + // TODO: Figure out API. + //.uniformVanilla3f(PER_FRAME, "heldBlockLightColor", mainHandSupplier::getLightColor) + //.uniformVanilla3f(PER_FRAME, "heldBlockLightColor2", offHandSupplier::getLightColor); + + uniforms.uniform1i("entityId", CapturedRenderingState.INSTANCE::getCurrentRenderedEntity, + CapturedRenderingState.INSTANCE.getEntityIdNotifier()); + + uniforms.uniform1i("blockEntityId", CapturedRenderingState.INSTANCE::getCurrentRenderedBlockEntity, + CapturedRenderingState.INSTANCE.getBlockEntityIdNotifier()); + } + + /** + * Provides the currently held item, and it's light value, in the given hand as a uniform. Uses the item.properties ID map to map the item + * to an integer, and the old hand light value to map offhand to main hand. + */ + private static class HeldItemSupplier { + private final InteractionHand hand; + private final Object2IntFunction itemIdMap; + private final boolean applyOldHandLight; + private int intID; + private int lightValue; + private Vector3f lightColor; + + HeldItemSupplier(InteractionHand hand, Object2IntFunction itemIdMap, boolean shouldApplyOldHandLight) { + this.hand = hand; + this.itemIdMap = itemIdMap; + this.applyOldHandLight = shouldApplyOldHandLight && hand == InteractionHand.MAIN_HAND; + } + + private void invalidate() { + intID = -1; + lightValue = 0; + lightColor = IrisItemLightProvider.DEFAULT_LIGHT_COLOR; + } + + public void update() { + EntityPlayer player = Minecraft.getMinecraft().thePlayer; + + if (player == null) { + // Not valid when the player doesn't exist + invalidate(); + return; + } +// ItemStack heldStack = player.getItemInHand(hand); + // TODO: Offhand + ItemStack heldStack = player.getHeldItem(); + + if (heldStack == null) { + invalidate(); + return; + } + + Item heldItem = heldStack.getItem(); + + if (heldItem == null) { + invalidate(); + return; + } + + ResourceLocation heldItemId = new ResourceLocation(Item.itemRegistry.getNameForObject(heldItem)); + intID = itemIdMap.applyAsInt(new NamespacedId(heldItemId.getResourceDomain(), heldItemId.getResourcePath())); + + IrisItemLightProvider lightProvider = (IrisItemLightProvider) heldItem; + lightValue = lightProvider.getLightEmission(Minecraft.getMinecraft().thePlayer, heldStack); + + if (applyOldHandLight) { + lightProvider = applyOldHandLighting(player, lightProvider); + } + + lightColor = lightProvider.getLightColor(Minecraft.getMinecraft().thePlayer, heldStack); + } + + private IrisItemLightProvider applyOldHandLighting(@NotNull EntityPlayer player, IrisItemLightProvider existing) { + // TODO: Offhand +// ItemStack offHandStack = player.getItemInHand(InteractionHand.OFF_HAND); + ItemStack offHandStack = null; + + if (offHandStack == null) { + return existing; + } + + Item offHandItem = offHandStack.getItem(); + + if (offHandItem == null) { + return existing; + } + + IrisItemLightProvider lightProvider = (IrisItemLightProvider) offHandItem; + int newEmission = lightProvider.getLightEmission(Minecraft.getMinecraft().thePlayer, offHandStack); + + if (lightValue < newEmission) { + lightValue = newEmission; + return lightProvider; + } + + return existing; + } + + public int getIntID() { + return intID; + } + + public int getLightValue() { + return lightValue; + } + + public Vector3f getLightColor() { + return lightColor; + } + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java b/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java new file mode 100644 index 000000000..ff4ef3e20 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java @@ -0,0 +1,150 @@ +package net.coderbot.iris.uniforms; + +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerControllerMP; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.entity.EntityLivingBase; +import org.joml.Math; +import org.joml.Vector3d; +import org.joml.Vector4f; + +public class IrisExclusiveUniforms { + public static void addIrisExclusiveUniforms(UniformHolder uniforms) { + WorldInfoUniforms.addWorldInfoUniforms(uniforms); + + //All Iris-exclusive uniforms (uniforms which do not exist in either OptiFine or ShadersMod) should be registered here. + uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "thunderStrength", IrisExclusiveUniforms::getThunderStrength); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "currentPlayerHealth", IrisExclusiveUniforms::getCurrentHealth); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "maxPlayerHealth", IrisExclusiveUniforms::getMaxHealth); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "currentPlayerHunger", IrisExclusiveUniforms::getCurrentHunger); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "maxPlayerHunger", () -> 20); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "currentPlayerAir", IrisExclusiveUniforms::getCurrentAir); + uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "maxPlayerAir", IrisExclusiveUniforms::getMaxAir); + uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "firstPersonCamera", IrisExclusiveUniforms::isFirstPersonCamera); + uniforms.uniform1b(UniformUpdateFrequency.PER_TICK, "isSpectator", IrisExclusiveUniforms::isSpectator); + uniforms.uniform3d(UniformUpdateFrequency.PER_FRAME, "eyePosition", IrisExclusiveUniforms::getEyePosition); + Vector4f zero = new Vector4f(0, 0, 0, 0); + // TODO: Iris Shaders +// uniforms.uniform4f(UniformUpdateFrequency.PER_TICK, "lightningBoltPosition", () -> { +// if (Minecraft.getMinecraft().theWorld != null) { +// return StreamSupport.stream(Minecraft.getMinecraft().theWorld.entitiesForRendering().spliterator(), false).filter(bolt -> bolt instanceof LightningBolt).findAny().map(bolt -> { +// Vector3d unshiftedCameraPosition = CameraUniforms.getUnshiftedCameraPosition(); +// Vec3 vec3 = bolt.getPosition(Minecraft.getMinecraft().getDeltaFrameTime()); +// return new Vector4f((float) (vec3.x - unshiftedCameraPosition.x), (float) (vec3.y - unshiftedCameraPosition.y), (float) (vec3.z - unshiftedCameraPosition.z), 1); +// }).orElse(zero); +// } else { +// return zero; +// } +// }); + } + + private static float getThunderStrength() { + // Note: Ensure this is in the range of 0 to 1 - some custom servers send out of range values. + return Math.clamp(0.0F, 1.0F, Minecraft.getMinecraft().theWorld.thunderingStrength); + } + + private static float getCurrentHealth() { + if (Minecraft.getMinecraft().thePlayer == null || !Minecraft.getMinecraft().playerController.gameIsSurvivalOrAdventure()) { + return -1; + } + + return Minecraft.getMinecraft().thePlayer.getHealth() / Minecraft.getMinecraft().thePlayer.getMaxHealth(); + } + + private static float getCurrentHunger() { + if (Minecraft.getMinecraft().thePlayer == null || !Minecraft.getMinecraft().playerController.gameIsSurvivalOrAdventure()) { + return -1; + } + + return Minecraft.getMinecraft().thePlayer.getFoodStats().getFoodLevel() / 20f; + } + + private static float getCurrentAir() { + if (Minecraft.getMinecraft().thePlayer == null || !Minecraft.getMinecraft().playerController.gameIsSurvivalOrAdventure()) { + return -1; + } + + return (float) Minecraft.getMinecraft().thePlayer.getAir() / (float) Minecraft.getMinecraft().thePlayer.getAir(); + } + + private static float getMaxAir() { + if (Minecraft.getMinecraft().thePlayer == null || !Minecraft.getMinecraft().playerController.gameIsSurvivalOrAdventure()) { + return -1; + } + +// return Minecraft.getMinecraft().thePlayer.getMaxAirSupply(); + return 300.0F; + } + + private static float getMaxHealth() { + if (Minecraft.getMinecraft().thePlayer == null || !Minecraft.getMinecraft().playerController.gameIsSurvivalOrAdventure()) { + return -1; + } + + return Minecraft.getMinecraft().thePlayer.getMaxHealth(); + } + + private static boolean isFirstPersonCamera() { + // If camera type is not explicitly third-person, assume it's first-person. + return (Minecraft.getMinecraft().gameSettings.thirdPersonView == 1); + } + + private static boolean isSpectator() { + final PlayerControllerMP controller = Minecraft.getMinecraft().playerController; + if(controller == null) + return false; + return controller.currentGameType.getID() == 3; + } + + private static Vector3d getEyePosition() { +// Objects.requireNonNull(Minecraft.getMinecraft().getCameraEntity()); +// return new Vector3d(Minecraft.getMinecraft().getCameraEntity().getX(), Minecraft.getMinecraft().getCameraEntity().getEyeY(), Minecraft.getMinecraft().getCameraEntity().getZ()); + final EntityLivingBase eye = Minecraft.getMinecraft().renderViewEntity; + return new Vector3d(eye.posX, eye.posY, eye.posZ); + } + + public static class WorldInfoUniforms { + public static void addWorldInfoUniforms(UniformHolder uniforms) { + final WorldClient level = Minecraft.getMinecraft().theWorld; + uniforms.uniform1i(UniformUpdateFrequency.PER_FRAME, "bedrockLevel", () -> 0); + uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "cloudHeight", () -> { + if (level != null && level.provider != null) { + return level.provider.getCloudHeight(); + } else { + return 192.0; + } + }); + uniforms.uniform1i(UniformUpdateFrequency.PER_FRAME, "heightLimit", () -> { + if (level != null && level.provider != null) { + return level.provider.getHeight(); + } else { + return 256; + } + }); + uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "hasCeiling", () -> { + if (level != null && level.provider != null) { + return level.provider.hasNoSky; + } else { + return false; + } + }); + uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "hasSkylight", () -> { + if (level != null && level.provider != null) { + return !level.provider.hasNoSky; + } else { + return true; + } + }); + uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "ambientLight", () -> { + if (level != null && level.provider != null) { + return level.provider.lightBrightnessTable[0]; + } else { + return 0f; + } + }); + + } + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/MatrixUniforms.java b/src/main/java/net/coderbot/iris/uniforms/MatrixUniforms.java new file mode 100644 index 000000000..34862d742 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/MatrixUniforms.java @@ -0,0 +1,94 @@ +package net.coderbot.iris.uniforms; + +import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.pipeline.ShadowRenderer; +import net.coderbot.iris.shaderpack.PackDirectives; +import net.coderbot.iris.shadow.ShadowMatrices; +import org.joml.Matrix4f; + +import java.util.function.Supplier; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; + +public final class MatrixUniforms { + private MatrixUniforms() { + } + + public static void addMatrixUniforms(UniformHolder uniforms, PackDirectives directives) { + addMatrix(uniforms, "ModelView", RenderingState.INSTANCE::getModelViewMatrix); + // TODO: In some cases, gbufferProjectionInverse takes on a value much different than OptiFine... + // We need to audit Mojang's linear algebra. + addMatrix(uniforms, "Projection", RenderingState.INSTANCE::getProjectionMatrix); + addShadowMatrix(uniforms, "ModelView", () -> ShadowRenderer.createShadowModelView(directives.getSunPathRotation(), directives.getShadowDirectives().getIntervalSize()).peek().getModel()); + addShadowMatrix(uniforms, "Projection", () -> ShadowMatrices.createOrthoMatrix(directives.getShadowDirectives().getDistance())); + } + + private static void addMatrix(UniformHolder uniforms, String name, Supplier supplier) { + uniforms + .uniformMatrix(PER_FRAME, "gbuffer" + name, supplier) + .uniformMatrix(PER_FRAME, "gbuffer" + name + "Inverse", new Inverted(supplier)) + .uniformMatrix(PER_FRAME, "gbufferPrevious" + name, new Previous(supplier)); + } + + private static void addShadowMatrix(UniformHolder uniforms, String name, Supplier supplier) { + uniforms + .uniformMatrix(PER_FRAME, "shadow" + name, supplier) + .uniformMatrix(PER_FRAME, "shadow" + name + "Inverse", new Inverted(supplier)); + } + + private static class Inverted implements Supplier { + private final Supplier parent; + + Inverted(Supplier parent) { + this.parent = parent; + } + + @Override + public Matrix4f get() { + // PERF: Don't copy + allocate this matrix every time? + final Matrix4f copy = new Matrix4f(parent.get()); + + copy.invert(); + + return copy; + } + } + + private static class InvertedArrayMatrix implements Supplier { + private final Supplier parent; + + InvertedArrayMatrix(Supplier parent) { + this.parent = parent; + } + + @Override + public Matrix4f get() { + final Matrix4f matrix4f = new Matrix4f().set(parent.get()); + matrix4f.invert(); + + return matrix4f; + } + } + + private static class Previous implements Supplier { + private final Supplier parent; + private Matrix4f previous; + + Previous(Supplier parent) { + this.parent = parent; + this.previous = new Matrix4f(); + } + + @Override + public Matrix4f get() { + // PERF: Don't copy + allocate these matrices every time? + final Matrix4f copy = new Matrix4f(parent.get()); + final Matrix4f prev = new Matrix4f(this.previous); + + this.previous = copy; + + return prev; + } + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/SystemTimeUniforms.java b/src/main/java/net/coderbot/iris/uniforms/SystemTimeUniforms.java new file mode 100644 index 000000000..4bf8ce6d1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/SystemTimeUniforms.java @@ -0,0 +1,112 @@ +package net.coderbot.iris.uniforms; + +import lombok.Getter; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; + +import java.util.OptionalLong; +import java.util.function.IntSupplier; + +/** + * Implements uniforms relating the system time (as opposed to the world time) + * + * @see Uniforms: System time + */ +public final class SystemTimeUniforms { + public static final Timer TIMER = new Timer(); + public static final FrameCounter COUNTER = new FrameCounter(); + + private SystemTimeUniforms() { + } + + /** + * Makes system time uniforms available to the given program + * + * @param uniforms the program to make the uniforms available to + */ + public static void addSystemTimeUniforms(UniformHolder uniforms) { + uniforms + .uniform1i(UniformUpdateFrequency.PER_FRAME, "frameCounter", COUNTER) + // TODO: Don't hardcode framemod8 here for Sildur's Vibrant Shaders + .uniform1i(UniformUpdateFrequency.PER_FRAME, "framemod8", () -> COUNTER.getAsInt() % 8) + .uniform1f(UniformUpdateFrequency.PER_FRAME, "frameTime", TIMER::getLastFrameTime) + .uniform1f(UniformUpdateFrequency.PER_FRAME, "frameTimeCounter", TIMER::getFrameTimeCounter); + } + + public static void addFloatFrameMod8Uniform(UniformHolder uniforms) { + uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "framemod8", () -> COUNTER.getAsInt() % 8); + } + + /** + * A simple frame counter. On each frame, it is incremented by 1, and it wraps around every 720720 frames. It starts + * at zero and goes from there. + */ + public static class FrameCounter implements IntSupplier { + private int count; + + private FrameCounter() { + this.count = 0; + } + + @Override + public int getAsInt() { + return count; + } + + public void beginFrame() { + count = (count + 1) % 720720; + } + + public void reset() { + count = 0; + } + } + + /** + * Keeps track of the time that the last frame took to render as well as the number of milliseconds since the start + * of the first frame to the start of the current frame. Updated at the start of each frame. + */ + public static final class Timer { + @Getter + private float frameTimeCounter; + @Getter + private float lastFrameTime; + + // Disabling this because OptionalLong provides a nice wrapper around (boolean valid, long value) + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private OptionalLong lastStartTime; + + public Timer() { + reset(); + } + + public void beginFrame(long frameStartTime) { + // Track how much time passed since the last time we began rendering a frame. + // If this is the first frame, then use a value of 0. + final long diffNs = frameStartTime - lastStartTime.orElse(frameStartTime); + // Convert to milliseconds + final long diffMs = (diffNs / 1000) / 1000; + + // Convert to seconds with a resolution of 1 millisecond, and store as the time taken for the last frame to complete. + lastFrameTime = diffMs / 1000.0F; + + // Advance the current frameTimeCounter by the amount of time the last frame took. + frameTimeCounter += lastFrameTime; + + // Prevent the frameTimeCounter from getting too large, since that causes issues with some shaderpacks + // This means that it should reset every hour. + if (frameTimeCounter >= 3600.0F) { + frameTimeCounter = 0.0F; + } + + // Finally, update the "last start time" value. + lastStartTime = OptionalLong.of(frameStartTime); + } + + public void reset() { + frameTimeCounter = 0.0F; + lastFrameTime = 0.0F; + lastStartTime = OptionalLong.empty(); + } + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/ViewportUniforms.java b/src/main/java/net/coderbot/iris/uniforms/ViewportUniforms.java new file mode 100644 index 000000000..199a1b0af --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/ViewportUniforms.java @@ -0,0 +1,38 @@ +package net.coderbot.iris.uniforms; + +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.minecraft.client.Minecraft; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_FRAME; + +/** + * Implements uniforms relating the current viewport + * + * @see Uniforms: Viewport + */ +public final class ViewportUniforms { + // cannot be constructed + private ViewportUniforms() { + } + + /** + * Makes the viewport uniforms available to the given program + * + * @param uniforms the program to make the uniforms available to + */ + public static void addViewportUniforms(UniformHolder uniforms) { + // TODO: What about the custom scale.composite3 property? + // NB: It is not safe to cache the render target due to mods like Resolution Control modifying the render target field. + uniforms + .uniform1f(PER_FRAME, "viewHeight", () -> Minecraft.getMinecraft().getFramebuffer().framebufferHeight) + .uniform1f(PER_FRAME, "viewWidth", () -> Minecraft.getMinecraft().getFramebuffer().framebufferWidth) + .uniform1f(PER_FRAME, "aspectRatio", ViewportUniforms::getAspectRatio); + } + + /** + * @return the current viewport aspect ratio, calculated from the current Minecraft window size + */ + private static float getAspectRatio() { + return ((float) Minecraft.getMinecraft().getFramebuffer().framebufferWidth) / ((float) Minecraft.getMinecraft().getFramebuffer().framebufferHeight); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/WorldTimeUniforms.java b/src/main/java/net/coderbot/iris/uniforms/WorldTimeUniforms.java new file mode 100644 index 000000000..170dc9477 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/WorldTimeUniforms.java @@ -0,0 +1,40 @@ +package net.coderbot.iris.uniforms; + +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; + +import java.util.Objects; + +import static net.coderbot.iris.gl.uniform.UniformUpdateFrequency.PER_TICK; + +public final class WorldTimeUniforms { + private WorldTimeUniforms() { + } + + /** + * Makes world time uniforms available to the given program + * + * @param uniforms the program to make the uniforms available to + */ + public static void addWorldTimeUniforms(UniformHolder uniforms) { + uniforms + .uniform1i(PER_TICK, "worldTime", WorldTimeUniforms::getWorldDayTime) + .uniform1i(PER_TICK, "worldDay", WorldTimeUniforms::getWorldDay) + .uniform1i(PER_TICK, "moonPhase", () -> getWorld().getMoonPhase()); + } + + static int getWorldDayTime() { + return (int) (getWorld().getWorldTime() % 24000L); + + // long dayTime = ((DimensionTypeAccessor) getWorld().dimensionType()).getFixedTime().orElse(timeOfDay % 24000L); + } + + private static int getWorldDay() { + return (int) (getWorld().getWorldTime() / 24000L); + } + + private static WorldClient getWorld() { + return Objects.requireNonNull(Minecraft.getMinecraft().theWorld); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/builtin/BuiltinReplacementUniforms.java b/src/main/java/net/coderbot/iris/uniforms/builtin/BuiltinReplacementUniforms.java new file mode 100644 index 000000000..7a3450abe --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/builtin/BuiltinReplacementUniforms.java @@ -0,0 +1,31 @@ +package net.coderbot.iris.uniforms.builtin; + +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import org.joml.Matrix4f; + +public class BuiltinReplacementUniforms { + private static final Matrix4f lightmapTextureMatrix; + + static { + // This mimics the transformations done in LightmapTextureManager to the GL_TEXTURE matrix. + lightmapTextureMatrix = new Matrix4f(); + lightmapTextureMatrix.identity(); + lightmapTextureMatrix.scale(0.00390625f); + + // TODO: Iris-Shaders - Is this logic correct? + final Matrix4f translateMatrix = new Matrix4f(); + translateMatrix.translate(8.0f, 8.0f, 8.0f); + lightmapTextureMatrix.mul(translateMatrix); + } + + public static void addBuiltinReplacementUniforms(UniformHolder uniforms) { + uniforms.uniformMatrix(UniformUpdateFrequency.ONCE, "iris_LightmapTextureMatrix", () -> { + Iris.logger.warn("A shader appears to require the lightmap texture matrix even after transformations have occurred"); + Iris.logger.warn("Iris handles this correctly but it indicates that the shader is doing weird things with lightmap coordinates"); + + return lightmapTextureMatrix; + }); + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedFloat.java b/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedFloat.java new file mode 100644 index 000000000..76ac51ca1 --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedFloat.java @@ -0,0 +1,141 @@ +package net.coderbot.iris.uniforms.transforms; + +import net.coderbot.iris.gl.uniform.FloatSupplier; +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import net.coderbot.iris.uniforms.SystemTimeUniforms; + +/** + * An implementation of basic exponential smoothing that converts a sequence of unsmoothed values into a sequence of + * smoothed values. + * + * @see + * Wikipedia: Basic (simple) exponential smoothing (Holt linear) + */ +public class SmoothedFloat implements FloatSupplier { + /** + * Natural logarithm of 2, ie. {@code ln(2)} + */ + private static final double LN_OF_2 = Math.log(2.0); + + /** + * The input sequence of unsmoothed values + */ + private final FloatSupplier unsmoothed; + + /** + * An accumulator for smoothed values. + */ + private float accumulator; + + /** + * Tracks whether an initial value has already been generated, because otherwise there will be nothing to smooth + * with. + */ + private boolean hasInitialValue; + + /** + * The decay constant upward, k (as used in e^(-kt)) + */ + private final float decayConstantUp; + + /** + * The decay constant downward, k (as used in e^(-kt)) + */ + private final float decayConstantDown; + + /** + * Creates a new SmoothedFloat with a given half life. + * + * @param halfLifeUp the half life in the exponential decay, in deciseconds (1/10th of a second) / 2 ticks. + * For example, a half life of value of 2.0 is 4 ticks or 0.2 seconds + * @param unsmoothed the input sequence of unsmoothed values to be smoothed. {@code unsmoothed.getAsFloat()} will be + * called exactly once for every time {@code smoothed.getAsFloat()} is called. + */ + public SmoothedFloat(float halfLifeUp, float halfLifeDown, FloatSupplier unsmoothed, FrameUpdateNotifier updateNotifier) { + // Half life is measured in units of 10ths of a second, or 2 ticks + // For example, a half life of value of 2.0 is 4 ticks or 0.2 seconds + this.decayConstantUp = computeDecay(halfLifeUp * 0.1F); + this.decayConstantDown = computeDecay(halfLifeDown * 0.1F); + + this.unsmoothed = unsmoothed; + + updateNotifier.addListener(this::update); + } + + /** + * Takes one value from the unsmoothed value sequence, and smooths it into our accumulator + */ + private void update() { + if (!hasInitialValue) { + // There is no smoothing on the first value. + // This is not an optimal approach to choosing the initial value: + // https://en.wikipedia.org/wiki/Exponential_smoothing#Choosing_the_initial_smoothed_value + // + // However, it works well enough for now. + accumulator = unsmoothed.getAsFloat(); + hasInitialValue = true; + + return; + } + + // Implements the basic variant of exponential smoothing + // https://en.wikipedia.org/wiki/Exponential_smoothing#Basic_(simple)_exponential_smoothing_(Holt_linear) + + // xₜ + float newValue = unsmoothed.getAsFloat(); + + // 𝚫t + float lastFrameTime = SystemTimeUniforms.TIMER.getLastFrameTime(); + + // Compute the smoothing factor based on our + // α = 1 - e^(-𝚫t/τ) = 1 - e^(-k𝚫t) + float smoothingFactor = 1.0f - exponentialDecayFactor(newValue > this.accumulator ? this.decayConstantUp : decayConstantDown, lastFrameTime); + + // sₜ = αxₜ + (1 - α)sₜ₋₁ + accumulator = lerp(accumulator, newValue, smoothingFactor); + } + + private float computeDecay(float halfLife) { + // Compute the decay constant from the half life + // https://en.wikipedia.org/wiki/Exponential_decay#Measuring_rates_of_decay + // https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant + // k = 1 / τ + return (float) (1.0f / (halfLife / LN_OF_2)); + } + + /** + * @return the current smoothed value + */ + @Override + public float getAsFloat() { + if (!hasInitialValue) { + return unsmoothed.getAsFloat(); + } + + return accumulator; + } + + /** + * Computes an exponential decay factor based on the given decay constant and time value + * + * @param k the decay constant, derived from the half life + * @param t the time that has passed since the decay started + */ + private static float exponentialDecayFactor(float k, float t) { + // https://en.wikipedia.org/wiki/Exponential_decay + // e^(-kt) + return (float) Math.exp(-k * t); + } + + /** + * Computes a linearly interpolated value between v0 and v1 + * + * @param v0 the starting value (t = 0) + * @param v1 the ending value (t = 1) + * @param t the time/progress value - should be in the range of 0.0 to 1.0 + */ + private static float lerp(float v0, float v1, float t) { + // https://en.wikipedia.org/wiki/Linear_interpolation + return (1 - t) * v0 + t * v1; + } +} diff --git a/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedVec2f.java b/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedVec2f.java new file mode 100644 index 000000000..aba7a642a --- /dev/null +++ b/src/main/java/net/coderbot/iris/uniforms/transforms/SmoothedVec2f.java @@ -0,0 +1,22 @@ +package net.coderbot.iris.uniforms.transforms; + +import net.coderbot.iris.uniforms.FrameUpdateNotifier; +import org.joml.Vector2f; +import org.joml.Vector2i; + +import java.util.function.Supplier; + +public class SmoothedVec2f implements Supplier { + private final SmoothedFloat x; + private final SmoothedFloat y; + + public SmoothedVec2f(float halfLifeUp, float halfLifeDown, Supplier unsmoothed, FrameUpdateNotifier updateNotifier) { + x = new SmoothedFloat(halfLifeUp, halfLifeDown, () -> unsmoothed.get().x, updateNotifier); + y = new SmoothedFloat(halfLifeUp, halfLifeDown, () -> unsmoothed.get().y, updateNotifier); + } + + @Override + public Vector2f get() { + return new Vector2f(x.getAsFloat(), y.getAsFloat()); + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/BlockSensitiveBufferBuilder.java b/src/main/java/net/coderbot/iris/vertices/BlockSensitiveBufferBuilder.java new file mode 100644 index 000000000..4a8d123c7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/BlockSensitiveBufferBuilder.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.vertices; + +public interface BlockSensitiveBufferBuilder { + void beginBlock(short block, short renderType, int localPosX, int localPosY, int localPosZ); + + void endBlock(); +} diff --git a/src/main/java/net/coderbot/iris/vertices/BufferBuilderPolygonView.java b/src/main/java/net/coderbot/iris/vertices/BufferBuilderPolygonView.java new file mode 100644 index 000000000..6bf47c07b --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/BufferBuilderPolygonView.java @@ -0,0 +1,42 @@ +package net.coderbot.iris.vertices; + +import java.nio.ByteBuffer; + +public class BufferBuilderPolygonView implements QuadView { + private ByteBuffer buffer; + private int writePointer; + private int stride = 48; + private int vertexAmount; + + public void setup(ByteBuffer buffer, int writePointer, int stride, int vertexAmount) { + this.buffer = buffer; + this.writePointer = writePointer; + this.stride = stride; + this.vertexAmount = vertexAmount; + } + + @Override + public float x(int index) { + return buffer.getFloat(writePointer - stride * (vertexAmount - index)); + } + + @Override + public float y(int index) { + return buffer.getFloat(writePointer + 4 - stride * (vertexAmount - index)); + } + + @Override + public float z(int index) { + return buffer.getFloat(writePointer + 8 - stride * (vertexAmount - index)); + } + + @Override + public float u(int index) { + return buffer.getFloat(writePointer + 16 - stride * (vertexAmount - index)); + } + + @Override + public float v(int index) { + return buffer.getFloat(writePointer + 20 - stride * (vertexAmount - index)); + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/ExtendedDataHelper.java b/src/main/java/net/coderbot/iris/vertices/ExtendedDataHelper.java new file mode 100644 index 000000000..4824927f0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/ExtendedDataHelper.java @@ -0,0 +1,20 @@ +package net.coderbot.iris.vertices; + +public final class ExtendedDataHelper { + // TODO: Resolve render types for normal blocks? + public static final short BLOCK_RENDER_TYPE = -1; + /** All fluids have a ShadersMod render type of 1, to match behavior of Minecraft 1.7 and earlier. */ + public static final short FLUID_RENDER_TYPE = 1; + + public static int packMidBlock(float x, float y, float z) { + return ((int) (x * 64) & 0xFF) | (((int) (y * 64) & 0xFF) << 8) | (((int) (z * 64) & 0xFF) << 16); + } + + public static int computeMidBlock(float x, float y, float z, int localPosX, int localPosY, int localPosZ) { + return packMidBlock( + localPosX + 0.5f - x, + localPosY + 0.5f - y, + localPosZ + 0.5f - z + ); + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/ExtendingBufferBuilder.java b/src/main/java/net/coderbot/iris/vertices/ExtendingBufferBuilder.java new file mode 100644 index 000000000..6814a49a0 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/ExtendingBufferBuilder.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.vertices; + +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; + +public interface ExtendingBufferBuilder { + void iris$beginWithoutExtending(int drawMode, VertexFormat vertexFormat); +} diff --git a/src/main/java/net/coderbot/iris/vertices/ImmediateState.java b/src/main/java/net/coderbot/iris/vertices/ImmediateState.java new file mode 100644 index 000000000..2762c6bda --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/ImmediateState.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.vertices; + +/** + * Some annoying global state needed for the extended vertex format disabling optimization. + */ +public class ImmediateState { + public static boolean isRenderingLevel = false; + public static boolean renderWithExtendedVertexFormat = true; +} diff --git a/src/main/java/net/coderbot/iris/vertices/IrisVertexFormats.java b/src/main/java/net/coderbot/iris/vertices/IrisVertexFormats.java new file mode 100644 index 000000000..4b7edf3f5 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/IrisVertexFormats.java @@ -0,0 +1,51 @@ +package net.coderbot.iris.vertices; + +import com.google.common.collect.ImmutableList; +import com.gtnewhorizons.angelica.compat.toremove.DefaultVertexFormat; +import com.gtnewhorizons.angelica.compat.toremove.VertexFormat; +import com.gtnewhorizons.angelica.compat.mojang.VertexFormatElement; + +public class IrisVertexFormats { + public static final VertexFormatElement ENTITY_ELEMENT; + public static final VertexFormatElement MID_TEXTURE_ELEMENT; + public static final VertexFormatElement TANGENT_ELEMENT; + public static final VertexFormatElement MID_BLOCK_ELEMENT; + + public static final VertexFormat TERRAIN; + public static final VertexFormat ENTITY; + + static { + ENTITY_ELEMENT = new VertexFormatElement(11, VertexFormatElement.Type.SHORT, VertexFormatElement.Usage.GENERIC, 2); + MID_TEXTURE_ELEMENT = new VertexFormatElement(12, VertexFormatElement.Type.FLOAT, VertexFormatElement.Usage.GENERIC, 2); + TANGENT_ELEMENT = new VertexFormatElement(13, VertexFormatElement.Type.BYTE, VertexFormatElement.Usage.GENERIC, 4); + MID_BLOCK_ELEMENT = new VertexFormatElement(14, VertexFormatElement.Type.BYTE, VertexFormatElement.Usage.GENERIC, 3); + + ImmutableList.Builder terrainElements = ImmutableList.builder(); + ImmutableList.Builder entityElements = ImmutableList.builder(); + + terrainElements.add(DefaultVertexFormat.POSITION_ELEMENT); // 12 + terrainElements.add(DefaultVertexFormat.COLOR_ELEMENT); // 16 + terrainElements.add(DefaultVertexFormat.TEXTURE_0_ELEMENT); // 24 + terrainElements.add(DefaultVertexFormat.LIGHT_ELEMENT); // 28 + terrainElements.add(DefaultVertexFormat.NORMAL_ELEMENT); // 31 + terrainElements.add(DefaultVertexFormat.PADDING_ELEMENT); // 32 + terrainElements.add(ENTITY_ELEMENT); // 36 + terrainElements.add(MID_TEXTURE_ELEMENT); // 44 + terrainElements.add(TANGENT_ELEMENT); // 48 + terrainElements.add(MID_BLOCK_ELEMENT); // 51 + terrainElements.add(DefaultVertexFormat.PADDING_ELEMENT); // 52 + + entityElements.add(DefaultVertexFormat.POSITION_ELEMENT); // 12 + entityElements.add(DefaultVertexFormat.COLOR_ELEMENT); // 16 + entityElements.add(DefaultVertexFormat.TEXTURE_0_ELEMENT); // 24 + entityElements.add(DefaultVertexFormat.OVERLAY_ELEMENT); // 28 + entityElements.add(DefaultVertexFormat.LIGHT_ELEMENT); // 32 + entityElements.add(DefaultVertexFormat.NORMAL_ELEMENT); // 35 + entityElements.add(DefaultVertexFormat.PADDING_ELEMENT); // 36 + entityElements.add(MID_TEXTURE_ELEMENT); // 44 + entityElements.add(TANGENT_ELEMENT); // 48 + + TERRAIN = new VertexFormat(terrainElements.build()); + ENTITY = new VertexFormat(entityElements.build()); + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/NormI8.java b/src/main/java/net/coderbot/iris/vertices/NormI8.java new file mode 100644 index 000000000..61bc793b8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/NormI8.java @@ -0,0 +1,97 @@ +package net.coderbot.iris.vertices; + +import org.joml.Vector3f; +import org.joml.Math; + +/** + * Provides some utilities for working with packed normal vectors. Each normal component provides 8 bits of + * precision in the range of [-1.0,1.0]. + * Copied from Sodium, licensed under the LGPLv3. Modified to support a W component. + * + * | 32 | 24 | 16 | 8 | + * | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 | + * | W | X | Y | Z | + */ +public class NormI8 { + private static final int X_COMPONENT_OFFSET = 0; + private static final int Y_COMPONENT_OFFSET = 8; + private static final int Z_COMPONENT_OFFSET = 16; + private static final int W_COMPONENT_OFFSET = 24; + + /** + * The maximum value of a normal's vector component. + */ + private static final float COMPONENT_RANGE = 127.0f; + + /** + * Constant value which can be multiplied with a floating-point vector component to get the normalized value. The + * multiplication is slightly faster than a floating point division, and this code is a hot path which justifies it. + */ + private static final float NORM = 1.0f / COMPONENT_RANGE; + + public static int pack(Vector3f normal) { + return pack(normal.x(), normal.y(), normal.z(), 0); + } + + /** + * Packs the specified vector components into a 32-bit integer in XYZ ordering with the 8 bits of padding at the + * end. + * @param x The x component of the normal's vector + * @param y The y component of the normal's vector + * @param z The z component of the normal's vector + */ + public static int pack(float x, float y, float z, float w) { + return ((int) (x * 127) & 0xFF) | (((int) (y * 127) & 0xFF) << 8) | (((int) (z * 127) & 0xFF) << 16) | (((int) (w * 127) & 0xFF) << 24); + } + /** + * Packs the specified vector components into a 32-bit integer in XYZ ordering with the 8 bits of padding at the + * end. + * @param x The x component of the normal's vector + * @param y The y component of the normal's vector + * @param z The z component of the normal's vector + */ + public static int packColor(float x, float y, float z, float w) { + return ((int) (x * 127) & 0xFF) | (((int) (y * 127) & 0xFF) << 8) | (((int) (z * 127) & 0xFF) << 16) | (((int) w & 0xFF) << 24); + } + + /** + * Encodes a float in the range of -1.0..1.0 to a normalized unsigned integer in the range of 0..255 which can then + * be passed to graphics memory. + */ + private static int encode(float comp) { + // TODO: is the clamp necessary here? our inputs should always be normalized vector components + return ((int) (Math.clamp(comp, -1.0F, 1.0F) * COMPONENT_RANGE) & 255); + } + + /** + * Unpacks the x-component of the packed normal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed normal + */ + public static float unpackX(int norm) { + return ((byte) ((norm >> X_COMPONENT_OFFSET) & 0xFF)) * NORM; + } + + /** + * Unpacks the y-component of the packed normal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed normal + */ + public static float unpackY(int norm) { + return ((byte) ((norm >> Y_COMPONENT_OFFSET) & 0xFF)) * NORM; + } + + /** + * Unpacks the z-component of the packed normal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed normal + */ + public static float unpackZ(int norm) { + return ((byte) ((norm >> Z_COMPONENT_OFFSET) & 0xFF)) * NORM; + } + + /** + * Unpacks the w-component of the packed normal, denormalizing it to a float in the range of -1.0..1.0. + * @param norm The packed normal + */ + public static float unpackW(int norm) { + return ((byte) ((norm >> W_COMPONENT_OFFSET) & 0xFF)) * NORM; + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/NormalHelper.java b/src/main/java/net/coderbot/iris/vertices/NormalHelper.java new file mode 100644 index 000000000..d37ac81d3 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/NormalHelper.java @@ -0,0 +1,345 @@ +package net.coderbot.iris.vertices; + +import org.joml.Vector3f; +import org.joml.Math; +import org.jetbrains.annotations.NotNull; + +public abstract class NormalHelper { + private NormalHelper() { } + + /** + * Stores a normal plus an extra value as a quartet of signed bytes. + * This is the same normal format that vanilla item rendering expects. + * The extra value is for use by shaders. + */ + public static int packNormal(float x, float y, float z, float w) { + x = Math.clamp(x, -1, 1); + y = Math.clamp(y, -1, 1); + z = Math.clamp(z, -1, 1); + w = Math.clamp(w, -1, 1); + + return ((int) (x * 127) & 0xFF) | (((int) (y * 127) & 0xFF) << 8) | (((int) (z * 127) & 0xFF) << 16) | (((int) (w * 127) & 0xFF) << 24); + } + + /** + * Version of {@link #packNormal(float, float, float, float)} that accepts a vector type. + */ + public static int packNormal(Vector3f normal, float w) { + return packNormal(normal.x, normal.y, normal.z, w); + } + + /** + * Retrieves values packed by {@link #packNormal(float, float, float, float)}. + * + *

Components are x, y, z, w - zero based. + */ + public static float getPackedNormalComponent(int packedNormal, int component) { + return ((byte) (packedNormal >> (8 * component))) / 127f; + } + + /** + * Computes the face normal of the given quad and saves it in the provided non-null vector. + * + *

Assumes counter-clockwise winding order, which is the norm. + * Expects convex quads with all points co-planar. + */ + public static void computeFaceNormal(@NotNull Vector3f saveTo, QuadView q) { +// final Direction nominalFace = q.nominalFace(); +// +// if (GeometryHelper.isQuadParallelToFace(nominalFace, q)) { +// Vec3i vec = nominalFace.getVector(); +// saveTo.set(vec.getX(), vec.getY(), vec.getZ()); +// return; +// } + + final float x0 = q.x(0); + final float y0 = q.y(0); + final float z0 = q.z(0); + final float x1 = q.x(1); + final float y1 = q.y(1); + final float z1 = q.z(1); + final float x2 = q.x(2); + final float y2 = q.y(2); + final float z2 = q.z(2); + final float x3 = q.x(3); + final float y3 = q.y(3); + final float z3 = q.z(3); + + final float dx0 = x2 - x0; + final float dy0 = y2 - y0; + final float dz0 = z2 - z0; + final float dx1 = x3 - x1; + final float dy1 = y3 - y1; + final float dz1 = z3 - z1; + + float normX = dy0 * dz1 - dz0 * dy1; + float normY = dz0 * dx1 - dx0 * dz1; + float normZ = dx0 * dy1 - dy0 * dx1; + + float l = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ); + + if (l != 0) { + normX /= l; + normY /= l; + normZ /= l; + } + + saveTo.set(normX, normY, normZ); + } + + /** + * Computes the face normal of the given tri and saves it in the provided non-null vector. + * + *

Assumes counter-clockwise winding order, which is the norm. + */ + public static void computeFaceNormalTri(@NotNull Vector3f saveTo, TriView t) { +// final Direction nominalFace = q.nominalFace(); +// +// if (GeometryHelper.isQuadParallelToFace(nominalFace, q)) { +// Vec3i vec = nominalFace.getVector(); +// saveTo.set(vec.getX(), vec.getY(), vec.getZ()); +// return; +// } + + final float x0 = t.x(0); + final float y0 = t.y(0); + final float z0 = t.z(0); + final float x1 = t.x(1); + final float y1 = t.y(1); + final float z1 = t.z(1); + final float x2 = t.x(2); + final float y2 = t.y(2); + final float z2 = t.z(2); + + // note: subtraction order is significant here because of how the cross product works. + // If we're wrong our calculated normal will be pointing in the opposite direction of how it should. + // This current order is similar enough to the order in the quad variant. + final float dx0 = x2 - x0; + final float dy0 = y2 - y0; + final float dz0 = z2 - z0; + final float dx1 = x0 - x1; + final float dy1 = y0 - y1; + final float dz1 = z0 - z1; + + float normX = dy0 * dz1 - dz0 * dy1; + float normY = dz0 * dx1 - dx0 * dz1; + float normZ = dx0 * dy1 - dy0 * dx1; + + float l = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ); + + if (l != 0) { + normX /= l; + normY /= l; + normZ /= l; + } + + saveTo.set(normX, normY, normZ); + } + + public static int computeTangentSmooth(float normalX, float normalY, float normalZ, TriView t) { + // Capture all of the relevant vertex positions + float x0 = t.x(0); + float y0 = t.y(0); + float z0 = t.z(0); + + float x1 = t.x(1); + float y1 = t.y(1); + float z1 = t.z(1); + + float x2 = t.x(2); + float y2 = t.y(2); + float z2 = t.z(2); + + // Project all vertices onto normal plane (for smooth normal support). Optionally skip this step for flat shading. + // Procedure: + // project v onto normal + // offset v by the projection to get the point on the plane + // project x0, y0, z0 onto normal + float d0 = x0 * normalX + y0 * normalY + z0 * normalZ; + float d1 = x1 * normalX + y1 * normalY + z1 * normalZ; + float d2 = x2 * normalX + y2 * normalY + z2 * normalZ; + + // offset x, y, z by the projection to get the projected point on the normal plane + x0 -= d0 * normalX; + y0 -= d0 * normalY; + z0 -= d0 * normalZ; + + x1 -= d1 * normalX; + y1 -= d1 * normalY; + z1 -= d1 * normalZ; + + x2 -= d2 * normalX; + y2 -= d2 * normalY; + z2 -= d2 * normalZ; + + + float edge1x = x1 - x0; + float edge1y = y1 - y0; + float edge1z = z1 - z0; + + float edge2x = x2 - x0; + float edge2y = y2 - y0; + float edge2z = z2 - z0; + + float u0 = t.u(0); + float v0 = t.v(0); + + float u1 = t.u(1); + float v1 = t.v(1); + + float u2 = t.u(2); + float v2 = t.v(2); + + float deltaU1 = u1 - u0; + float deltaV1 = v1 - v0; + float deltaU2 = u2 - u0; + float deltaV2 = v2 - v0; + + float fdenom = deltaU1 * deltaV2 - deltaU2 * deltaV1; + float f; + + if (fdenom == 0.0) { + f = 1.0f; + } else { + f = 1.0f / fdenom; + } + + float tangentx = f * (deltaV2 * edge1x - deltaV1 * edge2x); + float tangenty = f * (deltaV2 * edge1y - deltaV1 * edge2y); + float tangentz = f * (deltaV2 * edge1z - deltaV1 * edge2z); + float tcoeff = rsqrt(tangentx * tangentx + tangenty * tangenty + tangentz * tangentz); + tangentx *= tcoeff; + tangenty *= tcoeff; + tangentz *= tcoeff; + + float bitangentx = f * (-deltaU2 * edge1x + deltaU1 * edge2x); + float bitangenty = f * (-deltaU2 * edge1y + deltaU1 * edge2y); + float bitangentz = f * (-deltaU2 * edge1z + deltaU1 * edge2z); + float bitcoeff = rsqrt(bitangentx * bitangentx + bitangenty * bitangenty + bitangentz * bitangentz); + bitangentx *= bitcoeff; + bitangenty *= bitcoeff; + bitangentz *= bitcoeff; + + // predicted bitangent = tangent × normal + // Compute the determinant of the following matrix to get the cross product + // i j k + // tx ty tz + // nx ny nz + + // Be very careful when writing out complex multi-step calculations + // such as vector cross products! The calculation for pbitangentz + // used to be broken because it multiplied values in the wrong order. + + float pbitangentx = tangenty * normalZ - tangentz * normalY; + float pbitangenty = tangentz * normalX - tangentx * normalZ; + float pbitangentz = tangentx * normalY - tangenty * normalX; + + float dot = (bitangentx * pbitangentx) + (bitangenty * pbitangenty) + (bitangentz * pbitangentz); + float tangentW; + + if (dot < 0) { + tangentW = -1.0F; + } else { + tangentW = 1.0F; + } + + return NormI8.pack(tangentx, tangenty, tangentz, tangentW); + } + + public static int computeTangent(float normalX, float normalY, float normalZ, TriView t) { + // Capture all of the relevant vertex positions + float x0 = t.x(0); + float y0 = t.y(0); + float z0 = t.z(0); + + float x1 = t.x(1); + float y1 = t.y(1); + float z1 = t.z(1); + + float x2 = t.x(2); + float y2 = t.y(2); + float z2 = t.z(2); + + float edge1x = x1 - x0; + float edge1y = y1 - y0; + float edge1z = z1 - z0; + + float edge2x = x2 - x0; + float edge2y = y2 - y0; + float edge2z = z2 - z0; + + float u0 = t.u(0); + float v0 = t.v(0); + + float u1 = t.u(1); + float v1 = t.v(1); + + float u2 = t.u(2); + float v2 = t.v(2); + + float deltaU1 = u1 - u0; + float deltaV1 = v1 - v0; + float deltaU2 = u2 - u0; + float deltaV2 = v2 - v0; + + float fdenom = deltaU1 * deltaV2 - deltaU2 * deltaV1; + float f; + + if (fdenom == 0.0) { + f = 1.0f; + } else { + f = 1.0f / fdenom; + } + + float tangentx = f * (deltaV2 * edge1x - deltaV1 * edge2x); + float tangenty = f * (deltaV2 * edge1y - deltaV1 * edge2y); + float tangentz = f * (deltaV2 * edge1z - deltaV1 * edge2z); + float tcoeff = rsqrt(tangentx * tangentx + tangenty * tangenty + tangentz * tangentz); + tangentx *= tcoeff; + tangenty *= tcoeff; + tangentz *= tcoeff; + + float bitangentx = f * (-deltaU2 * edge1x + deltaU1 * edge2x); + float bitangenty = f * (-deltaU2 * edge1y + deltaU1 * edge2y); + float bitangentz = f * (-deltaU2 * edge1z + deltaU1 * edge2z); + float bitcoeff = rsqrt(bitangentx * bitangentx + bitangenty * bitangenty + bitangentz * bitangentz); + bitangentx *= bitcoeff; + bitangenty *= bitcoeff; + bitangentz *= bitcoeff; + + // predicted bitangent = tangent × normal + // Compute the determinant of the following matrix to get the cross product + // i j k + // tx ty tz + // nx ny nz + + // Be very careful when writing out complex multi-step calculations + // such as vector cross products! The calculation for pbitangentz + // used to be broken because it multiplied values in the wrong order. + + float pbitangentx = tangenty * normalZ - tangentz * normalY; + float pbitangenty = tangentz * normalX - tangentx * normalZ; + float pbitangentz = tangentx * normalY - tangenty * normalX; + + float dot = (bitangentx * pbitangentx) + (bitangenty * pbitangenty) + (bitangentz * pbitangentz); + float tangentW; + + if (dot < 0) { + tangentW = -1.0F; + } else { + tangentW = 1.0F; + } + + return packNormal(tangentx, tangenty, tangentz, tangentW); + } + + private static float rsqrt(float value) { + if (value == 0.0f) { + // You heard it here first, folks: 1 divided by 0 equals 1 + // In actuality, this is a workaround for normalizing a zero length vector (leaving it as zero length) + return 1.0f; + } else { + return (float) (1.0 / Math.sqrt(value)); + } + } +} diff --git a/src/main/java/net/coderbot/iris/vertices/PolygonView.java b/src/main/java/net/coderbot/iris/vertices/PolygonView.java new file mode 100644 index 000000000..017793cd9 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/PolygonView.java @@ -0,0 +1,13 @@ +package net.coderbot.iris.vertices; + +public interface PolygonView { + float x(int index); + + float y(int index); + + float z(int index); + + float u(int index); + + float v(int index); +} diff --git a/src/main/java/net/coderbot/iris/vertices/QuadView.java b/src/main/java/net/coderbot/iris/vertices/QuadView.java new file mode 100644 index 000000000..3db437790 --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/QuadView.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.vertices; + +/** + * Implementations of this class must support at least four vertices. + */ +public interface QuadView extends TriView { +} diff --git a/src/main/java/net/coderbot/iris/vertices/TriView.java b/src/main/java/net/coderbot/iris/vertices/TriView.java new file mode 100644 index 000000000..e33996f8b --- /dev/null +++ b/src/main/java/net/coderbot/iris/vertices/TriView.java @@ -0,0 +1,7 @@ +package net.coderbot.iris.vertices; + +/** + * Implementations of this class must support at least three vertices. + */ +public interface TriView extends PolygonView { +} diff --git a/src/main/java/net/irisshaders/iris/api/v0/IrisApi.java b/src/main/java/net/irisshaders/iris/api/v0/IrisApi.java new file mode 100644 index 000000000..cf4c52076 --- /dev/null +++ b/src/main/java/net/irisshaders/iris/api/v0/IrisApi.java @@ -0,0 +1,97 @@ +package net.irisshaders.iris.api.v0; + +import net.coderbot.iris.apiimpl.IrisApiV0Impl; + +/** + * The entry point to the Iris API, major version 0. This is currently the latest + * version of the API. + * + * To access the API, use {@link #getInstance()}. + */ +public interface IrisApi { + /** + * @since API v0.0 + */ + static IrisApi getInstance() { + return IrisApiV0Impl.INSTANCE; + } + + /** + * Gets the minor revision of this API. This is incremented when + * new methods are added without breaking API. Mods can check this + * if they wish to check whether given API calls are available on + * the currently installed Iris version. + * + * @return The current minor revision. Currently, revision 1. + */ + int getMinorApiRevision(); + + /** + * Checks whether a shader pack is currently in use and being used + * for rendering. If there is no shader pack enabled or a shader + * pack failed to compile and is therefore not in use, this will + * return false. + * + *

Mods that need to enable custom workarounds for shaders + * should use this method. + * + * @return Whether shaders are being used for rendering. + * @since {@link #getMinorApiRevision() API v0.0} + */ + boolean isShaderPackInUse(); + + /** + * Checks whether the shadow pass is currently being rendered. + * + *

Generally, mods won't need to call this function for much. + * Mods should be fine with things being rendered multiple times + * each frame from different camera perspectives. Often, there's + * a better approach to fixing bugs than calling this function. + * + *

Pretty much the main legitimate use for this function that + * I've seen is in a mod like Immersive Portals, where it has + * very custom culling that doesn't work when the Iris shadow + * pass is active. + * + *

Naturally, this function can only return true if + * {@link #isShaderPackInUse()} returns true. + * + * @return Whether Iris is currently rendering the shadow pass. + * @since API v0.0 + */ + boolean isRenderingShadowPass(); + + /** + * Opens the main Iris GUI screen. It's up to Iris to decide + * what this screen is, but generally this is the shader selection + * screen. + * + * This method takes and returns Objects instead of any concrete + * Minecraft screen class to avoid referencing Minecraft classes. + * Nevertheless, the passed parent must either be null, or an + * object that is a subclass of the appropriate {@code Screen} + * class for the given Minecraft version. + * + * @param parent The parent screen, an instance of the appropriate + * {@code Screen} class. + * @return A {@code Screen} class for the main Iris GUI screen. + * @since API v0.0 + */ + Object openMainIrisScreenObj(Object parent); + + /** + * Gets the language key of the main screen. Currently, this + * is "options.iris.shaderPackSelection". + * + * @return the language key, for use with {@code TranslatableText} + * / {@code TranslatableComponent} + * @since API v0.0 + */ + String getMainScreenLanguageKey(); + + /** + * Gets a config object that can edit the Iris configuration. + * @since API v0.0 + */ + IrisApiConfig getConfig(); +} diff --git a/src/main/java/net/irisshaders/iris/api/v0/IrisApiConfig.java b/src/main/java/net/irisshaders/iris/api/v0/IrisApiConfig.java new file mode 100644 index 000000000..66285a3d6 --- /dev/null +++ b/src/main/java/net/irisshaders/iris/api/v0/IrisApiConfig.java @@ -0,0 +1,22 @@ +package net.irisshaders.iris.api.v0; + +/** + * @since API v0.0 + */ +public interface IrisApiConfig { + /** + * Checks whether there is a shader pack loaded. Note that it is possible for a + * shader pack to be loaded, but not in use, if the shader pack failed to compile. + * You probably meant to call {@link IrisApi#isShaderPackInUse()}! + * + * @return Whether a shader pack was loaded from disk + * @since API v0.0 + */ + boolean areShadersEnabled(); + + /** + * Sets whether shaders are enabled or not, and then applies the change. + * @since API v0.0 + */ + void setShadersEnabledAndApply(boolean enabled); +} diff --git a/src/main/java/net/irisshaders/iris/api/v0/item/IrisItemLightProvider.java b/src/main/java/net/irisshaders/iris/api/v0/item/IrisItemLightProvider.java new file mode 100644 index 000000000..f697d53b6 --- /dev/null +++ b/src/main/java/net/irisshaders/iris/api/v0/item/IrisItemLightProvider.java @@ -0,0 +1,24 @@ +package net.irisshaders.iris.api.v0.item; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemStack; +import org.joml.Vector3f; + +public interface IrisItemLightProvider { + + Vector3f DEFAULT_LIGHT_COLOR = new Vector3f(1, 1, 1); + + default int getLightEmission(EntityPlayer player, ItemStack stack) { + if (stack.getItem() instanceof ItemBlock item) { + + return item.field_150939_a.getLightValue(); + } + + return 0; + } + + default Vector3f getLightColor(EntityPlayer player, ItemStack stack) { + return DEFAULT_LIGHT_COLOR; + } +} diff --git a/src/main/resources/META-INF/angelica_at.cfg b/src/main/resources/META-INF/angelica_at.cfg index 8932017b0..6c4318eef 100644 --- a/src/main/resources/META-INF/angelica_at.cfg +++ b/src/main/resources/META-INF/angelica_at.cfg @@ -33,3 +33,16 @@ public net.minecraft.client.renderer.texture.AbstractTexture field_110553_a # gl public net.minecraft.client.renderer.texture.TextureAtlasSprite field_147966_k # useAnisotropicFiltering public net.minecraft.client.renderer.texture.TextureMap func_147634_a(Lnet/minecraft/util/ResourceLocation;I)Lnet/minecraft/util/ResourceLocation; # ResourceLocation completeResourceLocation(ResourceLocation, int) + +public net.minecraft.client.gui.GuiVideoSettings field_146498_f # parentGuiScreen +public net.minecraft.entity.Entity field_70151_c # fire + +public net.minecraft.client.gui.GuiSlot field_148157_o # initialClickY +public net.minecraft.client.gui.GuiSlot field_148167_s # lastClicked +public net.minecraft.client.gui.GuiSlot field_148169_q # amountScrolled +public net.minecraft.client.gui.GuiSlot field_148170_p # scrollMultiplier +public net.minecraft.client.gui.GuiSlot field_148168_r # selectedElement +public net.minecraft.client.gui.GuiSlot field_148165_u # hasListHeader +public net.minecraft.client.gui.GuiSlot field_148161_k # mc +public net.minecraft.client.gui.GuiSlot func_148121_k()V # void bindAmountScrolled() +public net.minecraft.client.gui.GuiSlot func_148136_c(IIII)V # void overlayBackground() diff --git a/src/main/resources/META-INF/archaicfix_at.cfg b/src/main/resources/META-INF/archaicfix_at.cfg new file mode 100644 index 000000000..98506732f --- /dev/null +++ b/src/main/resources/META-INF/archaicfix_at.cfg @@ -0,0 +1,71 @@ +# TODO: most of these can be compile-time only +# bugs.MixinEnchantmentHelper +public net.minecraft.enchantment.EnchantmentHelper$IModifier +public net.minecraft.enchantment.EnchantmentHelper$ModifierDamage +public net.minecraft.enchantment.EnchantmentHelper$ModifierLiving +public net.minecraft.enchantment.EnchantmentHelper$HurtIterator +public net.minecraft.enchantment.EnchantmentHelper$DamageIterator +public net.minecraft.enchantment.EnchantmentHelper$ModifierDamage ()V +public net.minecraft.enchantment.EnchantmentHelper$ModifierLiving ()V +public net.minecraft.enchantment.EnchantmentHelper$HurtIterator ()V +public net.minecraft.enchantment.EnchantmentHelper$DamageIterator ()V + +public net.minecraft.client.gui.inventory.GuiContainerCreative$ContainerCreative + +public net.minecraft.client.gui.GuiButton field_146123_n # hovered +public-f net.minecraft.client.resources.SkinManager$2 field_152636_b + +public net.minecraft.nbt.NBTTagList field_74747_a # tagList + +public net.minecraft.network.play.server.S0CPacketSpawnPlayer field_148956_c +public net.minecraft.network.play.server.S0CPacketSpawnPlayer field_148953_d +public net.minecraft.network.play.server.S0CPacketSpawnPlayer field_148954_e + +public net.minecraft.network.play.server.S11PacketSpawnExperienceOrb field_148990_b +public net.minecraft.network.play.server.S11PacketSpawnExperienceOrb field_148991_c +public net.minecraft.network.play.server.S11PacketSpawnExperienceOrb field_148988_d + +public net.minecraft.client.renderer.RenderGlobal field_72768_k # sortedWorldRenderers +public net.minecraft.client.renderer.RenderGlobal field_72765_l # worldRenderers +public net.minecraft.client.renderer.RenderGlobal field_72751_K # renderersLoaded +public net.minecraft.client.renderer.RenderGlobal field_72739_F # renderDistanceChunks +public net.minecraft.client.renderer.RenderGlobal field_72742_B # maxBlockX +public net.minecraft.client.renderer.RenderGlobal field_72780_y # minBlockX +public net.minecraft.client.renderer.RenderGlobal field_72743_C # maxBlockY +public net.minecraft.client.renderer.RenderGlobal field_72779_z # minBlockY +public net.minecraft.client.renderer.RenderGlobal field_72737_D # maxBlockZ +public net.minecraft.client.renderer.RenderGlobal field_72741_A # minBlockZ +public net.minecraft.client.renderer.RenderGlobal field_72766_m # renderChunksWide +public net.minecraft.client.renderer.RenderGlobal field_72763_n # renderChunksTall +public net.minecraft.client.renderer.RenderGlobal field_72764_o # renderChunksDeep +public net.minecraft.client.renderer.RenderGlobal field_72769_h # theWorld +public net.minecraft.client.renderer.RenderGlobal field_72767_j # worldRenderersToUpdate +public net.minecraft.client.renderer.RenderGlobal field_72753_P # dummyRenderInt +public net.minecraft.client.renderer.RenderGlobal field_72744_L # renderersBeingClipped +public net.minecraft.client.renderer.RenderGlobal field_72745_M # renderersBeingOccluded +public net.minecraft.client.renderer.RenderGlobal field_72746_N # renderersBeingRendered +public net.minecraft.client.renderer.RenderGlobal field_72747_O # renderersSkippingRenderPass +public net.minecraft.client.renderer.RenderGlobal field_72777_q # mc +public net.minecraft.client.renderer.RenderGlobal field_72749_I # countEntitiesRendered +public net.minecraft.client.renderer.RenderGlobal field_72750_J # countEntitiesHidden +public net.minecraft.client.renderer.RenderGlobal field_72752_Q # worldRenderersCheckIndex +public net.minecraft.client.renderer.RenderGlobal field_147603_i # prevChunkSortX +public net.minecraft.client.renderer.RenderGlobal field_147600_j # prevChunkSortY +public net.minecraft.client.renderer.RenderGlobal field_147601_k # prevChunkSortZ +public net.minecraft.client.renderer.RenderGlobal field_147596_f # prevRenderSortX +public net.minecraft.client.renderer.RenderGlobal field_147597_g # prevRenderSortY +public net.minecraft.client.renderer.RenderGlobal field_147602_h # prevRenderSortZ +public net.minecraft.client.renderer.RenderGlobal field_72754_S # allRenderLists +public net.minecraft.client.renderer.RenderGlobal field_72757_g # frustumCheckOffset + +public net.minecraft.client.renderer.RenderGlobal func_72722_c(III)V # markRenderersForNewPosition +public net.minecraft.client.renderer.RenderGlobal func_72733_a(ID)V # renderAllRenderLists +public net.minecraft.client.renderer.RenderGlobal func_72712_a()V # loadRenderers +public net.minecraft.client.renderer.RenderGlobal func_72724_a(IIID)I # renderSortedRenderers + +public net.minecraft.client.renderer.WorldRenderer field_78915_A # isInitialized +public net.minecraft.client.renderer.EntityRenderer field_78510_Z #renderEndNanoTime + +public net.minecraft.client.multiplayer.PlayerControllerMP field_78779_k #currentGameType + +public net.minecraft.world.World field_72993_I # activeChunkSet \ No newline at end of file diff --git a/src/main/resources/META-INF/notfine_at.cfg b/src/main/resources/META-INF/notfine_at.cfg new file mode 100644 index 000000000..6ebee9470 --- /dev/null +++ b/src/main/resources/META-INF/notfine_at.cfg @@ -0,0 +1,8 @@ +# For changing gui background +public-f net.minecraft.client.gui.Gui field_110325_k #optionsBackground +# For regenerating the stars render list +public net.minecraft.client.renderer.RenderGlobal field_72772_v #starGLCallList +# For adjusting vanilla option enumerators +public-f net.minecraft.client.settings.GameSettings$Options field_148270_M #valueStep +# For supporting some strangely setup mods +public net.minecraft.block.BlockLeaves field_150129_M diff --git a/src/main/resources/assets/angelica/lang/en_US.lang b/src/main/resources/assets/angelica/lang/en_US.lang new file mode 100644 index 000000000..6af5f9db9 --- /dev/null +++ b/src/main/resources/assets/angelica/lang/en_US.lang @@ -0,0 +1,128 @@ +options.button.shader=Shaders... + +options.chunks=%s chunks +options.entityShadows=Entity Shadows +sodium.option_impact.low=Low +sodium.option_impact.medium=Medium +sodium.option_impact.high=High +sodium.option_impact.extreme=Extreme +sodium.option_impact.varies=Varies +sodium.options.pages.quality=Quality +sodium.options.pages.performance=Performance +sodium.options.pages.advanced=Advanced +sodium.options.view_distance.tooltip=The render distance controls how far away terrain will be rendered. Shorter distances mean that less terrain will be rendered, improving frame rates. +sodium.options.simulation_distance.tooltip=The simulation distance controls how far away terrain and entities will be loaded and ticked. Shorter distances can reduce the internal server's load and may improve frame rates. +sodium.options.brightness.tooltip=Controls the brightness (gamma) of the game. +sodium.options.clouds.name=Clouds +sodium.options.clouds.tooltip=Controls whether or not clouds will be visible. +sodium.options.gui_scale.tooltip=Sets the maximum scale factor to be used for the user interface. If 'auto' is used, then the largest scale factor will always be used. +sodium.options.fullscreen.tooltip=If enabled, the game will display in full-screen (if supported). +sodium.options.v_sync.tooltip=If enabled, the game's frame rate will be synchronized to the monitor's refresh rate, making for a generally smoother experience at the expense of overall input latency. This setting might reduce performance if your system is too slow. +sodium.options.fps_limit.tooltip=Limits the maximum number of frames per second. This can help reduce battery usage and general system load when multi-tasking. If V-Sync is enabled, this option will be ignored unless it is lower than your display's refresh rate. +sodium.options.view_bobbing.tooltip=If enabled, the player's view will sway and bob when moving around. Players who experience motion sickness while playing can benefit from disabling this. +sodium.options.attack_indicator.tooltip=Controls where the Attack Indicator is displayed on screen. +sodium.options.autosave_indicator.tooltip=If enabled, an indicator will be shown when the game is saving the world to disk. +sodium.options.graphics_quality.tooltip=The default graphics quality controls some legacy options and is necessary for mod compatibility. If the options below are left to "Default they will use this setting. +sodium.options.clouds_quality.tooltip=Controls the quality of rendered clouds in the sky. +sodium.options.weather_quality.tooltip=Controls the quality of rain and snow effects. +sodium.options.leaves_quality.name=Leaves Quality +sodium.options.leaves_quality.tooltip=Controls the quality of leaves. +sodium.options.grass_quality.name=Grass Quality +sodium.options.grass_quality.tooltip=Controls the quality of grass block sides. +sodium.options.particle_quality.name=Particle Quality +sodium.options.particle_quality.tooltip=Controls the maximum number of particles which can be present on screen at any one time. +sodium.options.smooth_lighting.tooltip=Controls whether blocks will be smoothly lit and shaded. This slightly increases the amount of time needed to re-build a chunk, but doesn't affect frame rates. +sodium.options.biome_blend.value=%s block(s) +sodium.options.biome_blend.tooltip=Controls the range which biomes will be sampled for block colorization. Higher values greatly increase the amount of time it takes to build chunks for diminishing improvements in quality. +sodium.options.entity_distance.tooltip=Controls how far away entities can render from the player. Higher values increase the render distance at the expense of frame rates. +sodium.options.entity_shadows.tooltip=If enabled, basic shadows will be rendered beneath mobs and other entities. +sodium.options.vignette.name=Vignette +sodium.options.vignette.tooltip=If enabled, a vignette effect will be rendered on the player's view. This is very unlikely to make a difference to frame rates unless you are fill-rate limited. +sodium.options.mipmap_levels.tooltip=Controls the number of mipmaps which will be used for block model textures. Higher values provide better rendering of blocks in the distance, but may adversely affect performance with many animated textures. +sodium.options.use_chunk_multidraw.name=Use Chunk Multi-Draw +sodium.options.use_chunk_multidraw.tooltip=Multi-draw allows multiple chunks to be rendered with fewer draw calls, greatly reducing CPU overhead when rendering the world while also potentially allowing for more efficient GPU utilization. This optimization may cause issues with some graphics drivers, so you should try disabling it if you are experiencing glitches. +sodium.options.use_vertex_objects.name=Use Vertex Array Objects +sodium.options.use_vertex_objects.tooltip=Helps to improve performance by moving information about how vertex data should be rendered into the driver, allowing it to better optimize for repeated rendering of the same objects. There is generally no reason to disable this unless you're using incompatible mods. +sodium.options.use_block_face_culling.name=Use Block Face Culling +sodium.options.use_block_face_culling.tooltip=If enabled, only the sides of blocks which are facing the camera will be submitted for rendering. This can eliminate a large number of block faces very early in the rendering process, saving memory bandwidth and time on the GPU. Some resource packs may have issues with this option, so try disabling it if you're seeing holes in blocks. +sodium.options.use_compact_vertex_format.name=Use Compact Vertex Format +sodium.options.use_compact_vertex_format.tooltip=If enabled, a more compact vertex format will be used for rendering chunks. This can reduce graphics memory usage and bandwidth requirements significantly, especially for integrated graphics cards, but can cause z-fighting with some resource packs due to how it reduces the precision of position and texture coordinate attributes. +sodium.options.use_fog_occlusion.name=Use Fog Occlusion +sodium.options.use_fog_occlusion.tooltip=If enabled, chunks which are determined to be fully hidden by fog effects will not be rendered, helping to improve performance. The improvement can be more dramatic when fog effects are heavier (such as while underwater), but it may cause undesirable visual artifacts between the sky and fog in some scenarios. +sodium.options.use_entity_culling.name=Use Entity Culling +sodium.options.use_entity_culling.tooltip=If enabled, entities determined not to be in any visible chunks will be skipped during rendering. This can help improve performance by avoiding the rendering of entities located underground or behind walls. +sodium.options.use_particle_culling.name=Use Particle Culling +sodium.options.use_particle_culling.tooltip=If enabled, only particles which are determined to be visible will be rendered. This can provide a significant improvement to frame rates when many particles are nearby. +sodium.options.animate_only_visible_textures.name=Animate Only Visible Textures +sodium.options.animate_only_visible_textures.tooltip=If enabled, only animated textures determined to be visible will be updated. This can provide a significant boost to frame rates on some hardware, especially with heavier resource packs. If you experience issues with some textures not being animated, try disabling this option. +sodium.options.translucency_sorting.name=Translucency Sorting +sodium.options.translucency_sorting.tooltip=If enabled, translucent blocks will be sorted correctly from back to front to provide better rendering, especially when multiple translucent blocks are in view and layered. This is a bit of an expensive operation. +sodium.options.cpu_render_ahead_limit.name=CPU Render-Ahead Limit +sodium.options.cpu_render_ahead_limit.tooltip=Specifies the maximum number of frames the CPU can be waiting on the GPU to finish rendering. Very low or high values may create frame rate instability. +sodium.options.cpu_render_ahead_limit.value=%s frame(s) +sodium.options.allow_direct_memory_access.name=Allow Direct Memory Access +sodium.options.allow_direct_memory_access.tooltip=If enabled, some critical code paths will be allowed to use direct memory access for performance. This often greatly reduces CPU overhead for chunk and entity rendering, but can make it harder to diagnose some bugs and crashes. You should only disable this if you've been asked to or otherwise know what you're doing. +sodium.options.ignore_driver_blacklist.name=Ignore Driver Blacklist +sodium.options.ignore_driver_blacklist.tooltip=If enabled, known incompatibilities with your hardware/driver configuration will be ignored, allowing you to enable options that may cause issues with your game. You should generally not touch this option unless you know exactly what you are doing. After changing this option, you must save, close, and then re-open the settings screen. +sodium.options.enable_memory_tracing.name=Enable Memory Tracing +sodium.options.enable_memory_tracing.tooltip=Debugging feature. If enabled, stack traces will be collected alongside memory allocations to help improve diagnostic information when memory leaks are detected. +sodium.options.performance_impact_string=Performance Impact: %s +sodium.options.use_persistent_mapping.name=Use Persistent Mapping +sodium.options.use_persistent_mapping.tooltip=If enabled, a small amount of memory will be persistently mapped for speeding up chunk updates and entity rendering.\n\nRequires OpenGL 4.4 or ARB_buffer_storage. +sodium.options.chunk_memory_allocator.name=Chunk Memory Allocator +sodium.options.chunk_memory_allocator.tooltip=Selects the memory allocator that will be used for chunk rendering.\n- ASYNC: Fastest option, works well with most modern graphics drivers.\n- SWAP: Fallback option for older graphics drivers. May increase memory usage significantly. +sodium.options.chunk_memory_allocator.async=Async +sodium.options.chunk_memory_allocator.swap=Swap +sodium.options.chunk_update_threads.name=Chunk Update Threads +sodium.options.chunk_update_threads.tooltip=Specifies the number of threads to use for chunk building. Using more threads can speed up chunk loading and update speed, but may negatively impact frame times. +sodium.options.always_defer_chunk_updates.name=Always Defer Chunk Updates +sodium.options.always_defer_chunk_updates.tooltip=If enabled, rendering will never wait for chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag in the world. +sodium.options.use_no_error_context.name=Use No Error Context +sodium.options.use_no_error_context.tooltip=If enabled, the OpenGL context will be created with error checking disabled. This may slightly improve performance, but it also increases the risk that the game will crash instead of gracefully handling OpenGL errors. You should disable this option if you are experiencing sudden unexplained crashes. +sodium.options.threads.value=%s thread(s) +sodium.options.default=Default +sodium.options.buttons.undo=Undo +sodium.options.buttons.apply=Apply +sodium.options.buttons.donate=Buy coffee for JellySquid! +options.framerate=%s FPS +iris.shaders.toggled=Toggled shaders to %s! +iris.shaders.disabled=Shaders disabled! +options.iris.apply=Apply +options.iris.refresh=Refresh +options.iris.openShaderPackFolder=Open Shader Pack Folder... +options.iris.shaderPackSettings=Shader Pack Settings... +options.iris.shaderPackList=Shader Pack List... +options.iris.refreshShaderPacks=Refresh Shader Packs +options.iris.shaderPackSelection=Shader Packs... +options.iris.shaderPackSelection.title=Shader Packs +options.iris.shaderPackSelection.addedPack=Added Shader Pack "%s" +options.iris.shaderPackSelection.addedPacks=Added %s Shader Packs +options.iris.shaderPackSelection.failedAdd=Files were not valid Shader Packs +options.iris.shaderPackSelection.failedAddSingle="%s" is not a valid Shader Pack +options.iris.shaderPackSelection.copyError=Could not add Shader Pack "%s" +options.iris.shaderPackSelection.copyErrorAlreadyExists="%s" is already in your Shader Packs folder! +options.iris.shaderPackOptions.tooManyFiles=Cannot import multiple Shader Settings files! +options.iris.shaderPackOptions.failedImport=Failed to import Shader Settings file "%s" +options.iris.shaderPackOptions.importedSettings=Imported Shader Settings from "%s" +options.iris.shaders.disabled=Shaders: Disabled +options.iris.shaders.enabled=Shaders: Enabled +options.iris.shaders.nonePresent=Shaders: No Packs Present +options.iris.back=Back +options.iris.reset=Reset +options.iris.reset.tooltip=Reset ALL options and apply? +options.iris.reset.tooltip.holdShift=Hold SHIFT and click to reset +options.iris.importSettings.tooltip=Import settings from file +options.iris.exportSettings.tooltip=Export applied settings to file +options.iris.mustDisableFullscreen=Please disable fullscreen first! +options.iris.setToDefault=Set option to default value? +options.iris.profile=Profile +options.iris.profile.custom=Custom +options.iris.shadowDistance=Max Shadow Distance +options.iris.shadowDistance.enabled=Allows you to change the maximum distance for shadows. Terrain and entities beyond this distance will not cast shadows. Lowering the shadow distance can significantly increase performance. +options.iris.shadowDistance.disabled=Your current shader pack has already set a render distance for shadows; you cannot change it. +options.iris.gui.hide=Hide GUI +options.iris.gui.show=Show GUI +pack.iris.select.title=Select +pack.iris.configure.title=Configure +label.iris.true=On +label.iris.false=Off diff --git a/src/main/resources/assets/iris/textures/gui/widgets.png b/src/main/resources/assets/iris/textures/gui/widgets.png new file mode 100644 index 000000000..d4a74a0e0 Binary files /dev/null and b/src/main/resources/assets/iris/textures/gui/widgets.png differ diff --git a/src/main/resources/assets/notfine/lang/de_DE.lang b/src/main/resources/assets/notfine/lang/de_DE.lang new file mode 100644 index 000000000..a35b5f6f9 --- /dev/null +++ b/src/main/resources/assets/notfine/lang/de_DE.lang @@ -0,0 +1,54 @@ +options.button.detail=Details... +options.button.other=Sonstige... +options.button.particle=Partikel... +options.button.sky=Atmosphäre... +options.off=Aus +options.on=An +options.cloud_height=Wolkenhöhe +options.cloud_scale=Wolkengröße +options.gui_background=Hintergrund +options.gui_background.-1=Standard +options.gui_background.0=Sand +options.gui_background.1=Myzel +options.gui_background.2=Steinziegel +options.gui_background.3=Bemooste Steinziegel +options.gui_background.4=Eichenholzbretter +options.gui_background.5=Birkenholzbretter +options.guiScale.massive=Riesig +options.mode_cloud_translucency=Wolkendurchlässigkeit +options.mode_cloud_translucency.-1=Standard +options.mode_cloud_translucency.0=Immer +options.mode_cloud_translucency.1=Niemals +options.mode_clouds=Wolken +options.mode_clouds.-1=Standard +options.mode_clouds.0=Schön +options.mode_clouds.1=Schnell +options.mode_clouds.2=Aus +options.mode_glint_inv=Inventar-Verzauberungseffekt +options.mode_glint_world=Welt-Verzauberungseffekt +options.mode_gui_background=Menühintergründe +options.mode_leaves=Laub +options.mode_leaves.-1=Standard +options.mode_leaves.0=Schön +options.mode_leaves.1=Schnell +options.mode_leaves.2=Schlau +options.mode_leaves.3=Schön [Hybrid] +options.mode_leaves.4=Schnell [Hybrid] +options.mode_shadows=Objektschatten +options.mode_shadows.-1=Standard +options.mode_shadows.0=An +options.mode_shadows.1=Aus +options.mode_sky=Himmel +options.mode_vignette=Vignette +options.mode_vignette.-1=Standard +options.mode_vignette.0=An +options.mode_vignette.1=Aus +options.particles_enc_table=Verzauberungspartikel +options.particles_void=Leerenpartikel +options.render_distance_clouds=Wolken-Renderdistanz +options.title.detail=Detaileinstellungen | NotFine alpha +options.title.other=Sonstige Einstellungen | NotFine alpha +options.title.particle=Partikeleinstellungen | NotFine alpha +options.title.sky=Atmosphäreneinstellungen | NotFine alpha +options.title.video=Grafikeinstellungen | NotFine alpha +options.total_stars=Sternendichte diff --git a/src/main/resources/assets/notfine/lang/en_US.lang b/src/main/resources/assets/notfine/lang/en_US.lang new file mode 100644 index 000000000..b394bde54 --- /dev/null +++ b/src/main/resources/assets/notfine/lang/en_US.lang @@ -0,0 +1,63 @@ +options.button.detail=Details... +options.button.other=Other... +options.button.particle=Particles... +options.button.sky=Atmosphere... +options.off=Off +options.on=On +options.cloud_height=Cloud Height +options.cloud_scale=Cloud Scale +options.gui_background=Background +options.gui_background.-1=Default +options.gui_background.0=Sand +options.gui_background.1=Mycelium +options.gui_background.2=Stonebrick +options.gui_background.3=Mossy Stonebrick +options.gui_background.4=Oak Planks +options.gui_background.5=Birch Planks +options.guiScale.massive=Massive +options.mode_cloud_translucency=Cloud Translucency +options.mode_cloud_translucency.-1=Default +options.mode_cloud_translucency.0=Always +options.mode_cloud_translucency.1=Never +options.mode_clouds=Clouds +options.mode_clouds.-1=Default +options.mode_clouds.0=Fancy +options.mode_clouds.1=Fast +options.mode_clouds.2=Off +options.mode_dropped_items=Dropped Items +options.mode_dropped_items.-1=Default +options.mode_dropped_items.0=Fancy +options.mode_dropped_items.1=Fast +options.mode_glint_inv=GUI Enchant VFX +options.mode_glint_world=World Enchant VFX +options.mode_gui_background=Menu Backgrounds +options.mode_leaves=Foliage +options.mode_leaves.-1=Default +options.mode_leaves.0=Fancy +options.mode_leaves.1=Fast +options.mode_leaves.2=Smart +options.mode_leaves.3=Hybrid Fancy +options.mode_leaves.4=Hybrid Fast +options.mode_shadows=Entity Shadows +options.mode_shadows.-1=Default +options.mode_shadows.0=On +options.mode_shadows.1=Off +options.mode_sky=Sky +options.mode_water=Water +options.mode_water.-1=Default +options.mode_water.0=Fancy +options.mode_water.1=Fast +options.mode_vignette=Vignette +options.mode_vignette.-1=Default +options.mode_vignette.0=On +options.mode_vignette.1=Off +options.particles_enc_table=Enchantment Particles +options.particles_void=Void Particles +options.render_distance_clouds=Cloud Render Distance +options.title.detail=Detail Settings | NotFine alpha +options.title.other=Other Settings | NotFine alpha +options.title.particle=Particle Settings | NotFine alpha +options.title.sky=Atmosphere Settings | NotFine alpha +options.title.video=Video Settings | NotFine alpha +options.total_stars=Star Density +options.render_distance_entities=Entity Render Mult diff --git a/src/main/resources/assets/notfine/lang/zh_CN.lang b/src/main/resources/assets/notfine/lang/zh_CN.lang new file mode 100644 index 000000000..5ed058622 --- /dev/null +++ b/src/main/resources/assets/notfine/lang/zh_CN.lang @@ -0,0 +1,54 @@ +options.button.detail=细节设置 +options.button.other=其他设置 +options.button.particle=粒子设置 +options.button.sky=大气设置 +options.off=关闭 +options.on=开启 +options.cloud_height=云高度 +options.cloud_scale=云优化 +options.gui_background=背景 +options.gui_background.-1=默认 +options.gui_background.0=沙子 +options.gui_background.1=菌丝 +options.gui_background.2=石砖 +options.gui_background.3=苔石砖 +options.gui_background.4=橡木木板 +options.gui_background.5=白桦木板 +options.guiScale.massive=巨大 +options.mode_cloud_translucency=云半透明 +options.mode_cloud_translucency.-1=默认 +options.mode_cloud_translucency.0=总是 +options.mode_cloud_translucency.1=从不 +options.mode_clouds=云 +options.mode_clouds.-1=默认 +options.mode_clouds.0=精致 +options.mode_clouds.1=快速 +options.mode_clouds.2=关闭 +options.mode_glint_inv=GUI附魔效果 +options.mode_glint_world=世界附魔效果 +options.mode_gui_background=菜单背景 +options.mode_leaves=树叶 +options.mode_leaves.-1=默认 +options.mode_leaves.0=精致 +options.mode_leaves.1=快速 +options.mode_leaves.2=智能 +options.mode_leaves.3=精致混合 +options.mode_leaves.4=快速混合 +options.mode_shadows=实体阴影 +options.mode_shadows.-1=默认 +options.mode_shadows.0=开启 +options.mode_shadows.1=关闭 +options.mode_sky=天空 +options.mode_vignette=晕影 +options.mode_vignette.-1=默认 +options.mode_vignette.0=开启 +options.mode_vignette.1=关闭 +options.particles_enc_table=附魔粒子 +options.particles_void=虚空粒子 +options.render_distance_clouds=云渲染距离 +options.title.detail=细节设置 | NotFine alpha +options.title.other=其他设置 | NotFine alpha +options.title.particle=粒子设置 | NotFine alpha +options.title.sky=大气设置 | NotFine alpha +options.title.video=视频设置 | NotFine alpha +options.total_stars=星星密度 diff --git a/src/main/resources/assets/sodium/lang/es_ar.json b/src/main/resources/assets/sodium/lang/es_ar.json new file mode 100644 index 000000000..a6847a8a5 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/es_ar.json @@ -0,0 +1,66 @@ +{ + "sodium.option_impact.low": "Bajo", + "sodium.option_impact.medium": "Medio", + "sodium.option_impact.high": "Alto", + "sodium.option_impact.extreme": "Extremo", + "sodium.option_impact.varies": "Varía", + "sodium.options.pages.quality": "Calidad", + "sodium.options.pages.performance": "Rendimiento", + "sodium.options.pages.advanced": "Avanzado", + "sodium.options.view_distance.tooltip": "La distancia de renderizado controla la distancia a la que se renderizará el terreno. Las distancias más cortas significan que se renderizará menos terreno, lo que mejora la velocidad de fotogramas.", + "sodium.options.simulation_distance.tooltip": "La distancia de simulación controla la distancia a la que se cargarán y marcarán el terreno y las entidades. Las distancias más cortas pueden reducir la carga del servidor interno y pueden mejorar la velocidad de fotogramas.", + "sodium.options.brightness.tooltip": "Controla el brillo (gamma) del juego.", + "sodium.options.gui_scale.tooltip": "Establece el factor de escala máximo que se utilizará para la interfaz de usuario. Si se utiliza 'auto', siempre se utilizará el factor de escala más grande.", + "sodium.options.fullscreen.tooltip": "Si se activa, el juego se mostrará en pantalla completa (si es compatible).", + "sodium.options.v_sync.tooltip": "Si se activa, la velocidad de fotogramas del juego se sincronizará con la frecuencia de refresco del monitor, lo que hará que la experiencia sea más fluida a expensas de la latencia de entrada general. Este ajuste puede reducir el rendimiento si tu sistema es demasiado lento.", + "sodium.options.fps_limit.tooltip": "Limita el número máximo de fotogramas por segundo. Esto puede ayudar a reducir el uso de la batería y la carga general del sistema cuando se realiza una multitarea. Si V-Sync está activado, esta opción se ignorará a menos que sea inferior a la frecuencia de refresco de tu pantalla.", + "sodium.options.view_bobbing.tooltip": "Si está activada, la vista del jugador se balanceará y oscilará cuando se mueva. Los jugadores que experimenten mareos al jugar pueden beneficiarse de desactivar esta opción.", + "sodium.options.attack_indicator.tooltip": "Controla dónde se muestra el indicador de ataque en la pantalla.", + "sodium.options.autosave_indicator.tooltip": "Si se activa, se mostrará un indicador cuando el juego esté guardando el mundo en el disco.", + "sodium.options.graphics_quality.tooltip": "La calidad gráfica por defecto controla algunas opciones heredadas y es necesaria para la compatibilidad de los mods. Si las opciones de abajo se dejan en \"Default\", utilizarán esta configuración.", + "sodium.options.clouds_quality.tooltip": "Controla la calidad de las nubes renderizadas en el cielo.", + "sodium.options.weather_quality.tooltip": "Controla la calidad de los efectos de lluvia y nieve.", + "sodium.options.leaves_quality.name": "Calidad de las Hojas", + "sodium.options.leaves_quality.tooltip": "Controla la calidad de las hojas.", + "sodium.options.particle_quality.name": "Calidad de las Partículas", + "sodium.options.particle_quality.tooltip": "Controla el número máximo de partículas que pueden estar presentes en la pantalla en un momento dado.", + "sodium.options.smooth_lighting.tooltip": "Controla si los bloques serán iluminados y sombreados suavemente. Esto aumenta ligeramente el tiempo necesario para reconstruir un trozo, pero no afecta a la velocidad de fotogramas.", + "sodium.options.biome_blend.value": "%s bloque(s)", + "sodium.options.biome_blend.tooltip": "Controla el rango que los biomas serán muestreados para la coloración de los bloques. Los valores más altos aumentan en gran medida la cantidad de tiempo que se necesita para construir trozos para disminuir las mejoras en la calidad.", + "sodium.options.entity_distance.tooltip": "Controla la distancia a la que se pueden renderizar las entidades desde el jugador. Los valores más altos aumentan la distancia de renderizado a expensas de la velocidad de fotogramas.", + "sodium.options.entity_shadows.tooltip": "Si está activada, las sombras básicas se mostrarán debajo de los monstruos y otras entidades.", + "sodium.options.vignette.name": "Viñeta", + "sodium.options.vignette.tooltip": "Si se habilita, se renderizará un efecto de viñeta en la vista del jugador. Es muy poco probable que esto suponga una diferencia en la tasa de fotogramas, a menos que la tasa de llenado sea limitada.", + "sodium.options.mipmap_levels.tooltip": "Controla el número de mipmaps que se utilizarán para las texturas del modelo de bloques. Los valores más altos proporcionan un mejor renderizado de los bloques en la distancia, pero pueden afectar negativamente al rendimiento con muchas texturas animadas.", + "sodium.options.use_block_face_culling.name": "Usar eliminación selectiva de caras de bloques", + "sodium.options.use_block_face_culling.tooltip": "Si se activa, sólo se enviarán a renderizar las caras de los bloques que estén orientadas hacia la cámara. Esto puede eliminar un gran número de caras de bloques muy pronto en el proceso de renderizado, ahorrando ancho de banda de memoria y tiempo en la GPU. Algunos paquetes de recursos pueden tener problemas con esta opción, así que prueba a desactivarla si ves agujeros en los bloques.", + "sodium.options.use_fog_occlusion.name": "Usar Oclusión de Niebla", + "sodium.options.use_fog_occlusion.tooltip": "Si se activa, los trozos que se determinan como totalmente ocultos por los efectos de niebla no se renderizarán, lo que ayuda a mejorar el rendimiento. La mejora puede ser más dramática cuando los efectos de niebla son más pesados (como cuando se está bajo el agua), pero puede causar artefactos visuales indeseables entre el cielo y la niebla en algunos escenarios.", + "sodium.options.use_entity_culling.name": "Usar Eliminación de Entidades", + "sodium.options.use_entity_culling.tooltip": "Si se activa, las entidades que se determinen que no están en ningún trozo visible se omitirán durante el renderizado. Esto puede ayudar a mejorar el rendimiento al evitar la renderización de las entidades situadas bajo tierra o detrás de las paredes.", + "sodium.options.use_particle_culling.name": "Utilice la Eliminación de Partículas", + "sodium.options.use_particle_culling.tooltip": "Si se activa, sólo se renderizarán las partículas que se determinen como visibles. Esto puede proporcionar una mejora significativa en la velocidad de fotogramas cuando hay muchas partículas cerca.", + "sodium.options.animate_only_visible_textures.name": "Animar Sólo las Texturas Visibles", + "sodium.options.animate_only_visible_textures.tooltip": "Si se activa, sólo se actualizarán las texturas animadas que sean visibles. Esto puede proporcionar un aumento significativo de la velocidad de fotogramas en algunos equipos, especialmente con paquetes de recursos más pesados. Si tienes problemas con algunas texturas que no se animan, prueba a desactivar esta opción.", + "sodium.options.cpu_render_ahead_limit.name": "Límite de Procesamiento Anticipado de CPU", + "sodium.options.cpu_render_ahead_limit.tooltip": "Especifica el número máximo de fotogramas que la CPU puede esperar a que la GPU termine de renderizar. Los valores muy bajos o altos pueden crear inestabilidad en la tasa de fotogramas.", + "sodium.options.cpu_render_ahead_limit.value": "%s fotograma(s)", + "sodium.options.allow_direct_memory_access.name": "Permitir Aacceso Directo a la Memoria", + "sodium.options.allow_direct_memory_access.tooltip": "Si se activa, se permitirá que algunas rutas de código críticas utilicen el acceso directo a la memoria para mejorar el rendimiento. Esto suele reducir en gran medida la sobrecarga de la CPU para el renderizado de trozos y entidades, pero puede dificultar el diagnóstico de algunos fallos y caídas. Sólo deberías desactivar esto si te lo han pedido o si sabes lo que estás haciendo.", + "sodium.options.enable_memory_tracing.name": "Habilitar Seguimiento de Memoria", + "sodium.options.enable_memory_tracing.tooltip": "Función de depuración. Si está activada, se recogerán los rastros de pila junto con las asignaciones de memoria para ayudar a mejorar la información de diagnóstico cuando se detecten fugas de memoria.", + "sodium.options.performance_impact_string": "Impacto en el Rendimiento: %s", + "sodium.options.use_persistent_mapping.name": "Utilizar el Mapeo Persistente", + "sodium.options.use_persistent_mapping.tooltip": "Si se activa, una pequeña cantidad de memoria será mapeada de forma persistente para acelerar las actualizaciones de chunk y el renderizado de entidades.\n\nRequiere OpenGL 4.4 o ARB_buffer_storage.", + "sodium.options.chunk_memory_allocator.name": "Asignador de Memoria de Chunks", + "sodium.options.chunk_memory_allocator.tooltip": "Selecciona el asignador de memoria que se utilizará para el renderizado de los trozos.\n- ASYNC: Opción más rápida, funciona bien con la mayoría de los controladores gráficos modernos.\n- SWAP: Opción de reserva para los controladores gráficos más antiguos. Puede aumentar significativamente el uso de la memoria.", + "sodium.options.chunk_memory_allocator.async": "Async", + "sodium.options.chunk_memory_allocator.swap": "Intercambiar", + "sodium.options.chunk_update_threads.name": "Subprocesos de Actualización de Chunks", + "sodium.options.chunk_update_threads.tooltip": "Especifica el número de hilos que se van a utilizar para la construcción de los trozos. El uso de más hilos puede acelerar la carga de trozos y la velocidad de actualización, pero puede afectar negativamente a los tiempos de fotogramas.", + "sodium.options.always_defer_chunk_updates.name": "Aplazar Siempre las Actualizaciones de Chunks", + "sodium.options.always_defer_chunk_updates.tooltip": "Si se activa, el renderizado nunca esperará a que terminen las actualizaciones de los chunk, incluso si son importantes. Esto puede mejorar mucho la velocidad de fotogramas en algunos escenarios, pero puede crear un retraso visual significativo en el mundo.", + "sodium.options.buttons.undo": "Deshacer", + "sodium.options.buttons.apply": "Aplicar", + "sodium.options.buttons.donate": "¡Compra café para JellySquid!" +} diff --git a/src/main/resources/assets/sodium/lang/es_es.json b/src/main/resources/assets/sodium/lang/es_es.json new file mode 100644 index 000000000..a6847a8a5 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/es_es.json @@ -0,0 +1,66 @@ +{ + "sodium.option_impact.low": "Bajo", + "sodium.option_impact.medium": "Medio", + "sodium.option_impact.high": "Alto", + "sodium.option_impact.extreme": "Extremo", + "sodium.option_impact.varies": "Varía", + "sodium.options.pages.quality": "Calidad", + "sodium.options.pages.performance": "Rendimiento", + "sodium.options.pages.advanced": "Avanzado", + "sodium.options.view_distance.tooltip": "La distancia de renderizado controla la distancia a la que se renderizará el terreno. Las distancias más cortas significan que se renderizará menos terreno, lo que mejora la velocidad de fotogramas.", + "sodium.options.simulation_distance.tooltip": "La distancia de simulación controla la distancia a la que se cargarán y marcarán el terreno y las entidades. Las distancias más cortas pueden reducir la carga del servidor interno y pueden mejorar la velocidad de fotogramas.", + "sodium.options.brightness.tooltip": "Controla el brillo (gamma) del juego.", + "sodium.options.gui_scale.tooltip": "Establece el factor de escala máximo que se utilizará para la interfaz de usuario. Si se utiliza 'auto', siempre se utilizará el factor de escala más grande.", + "sodium.options.fullscreen.tooltip": "Si se activa, el juego se mostrará en pantalla completa (si es compatible).", + "sodium.options.v_sync.tooltip": "Si se activa, la velocidad de fotogramas del juego se sincronizará con la frecuencia de refresco del monitor, lo que hará que la experiencia sea más fluida a expensas de la latencia de entrada general. Este ajuste puede reducir el rendimiento si tu sistema es demasiado lento.", + "sodium.options.fps_limit.tooltip": "Limita el número máximo de fotogramas por segundo. Esto puede ayudar a reducir el uso de la batería y la carga general del sistema cuando se realiza una multitarea. Si V-Sync está activado, esta opción se ignorará a menos que sea inferior a la frecuencia de refresco de tu pantalla.", + "sodium.options.view_bobbing.tooltip": "Si está activada, la vista del jugador se balanceará y oscilará cuando se mueva. Los jugadores que experimenten mareos al jugar pueden beneficiarse de desactivar esta opción.", + "sodium.options.attack_indicator.tooltip": "Controla dónde se muestra el indicador de ataque en la pantalla.", + "sodium.options.autosave_indicator.tooltip": "Si se activa, se mostrará un indicador cuando el juego esté guardando el mundo en el disco.", + "sodium.options.graphics_quality.tooltip": "La calidad gráfica por defecto controla algunas opciones heredadas y es necesaria para la compatibilidad de los mods. Si las opciones de abajo se dejan en \"Default\", utilizarán esta configuración.", + "sodium.options.clouds_quality.tooltip": "Controla la calidad de las nubes renderizadas en el cielo.", + "sodium.options.weather_quality.tooltip": "Controla la calidad de los efectos de lluvia y nieve.", + "sodium.options.leaves_quality.name": "Calidad de las Hojas", + "sodium.options.leaves_quality.tooltip": "Controla la calidad de las hojas.", + "sodium.options.particle_quality.name": "Calidad de las Partículas", + "sodium.options.particle_quality.tooltip": "Controla el número máximo de partículas que pueden estar presentes en la pantalla en un momento dado.", + "sodium.options.smooth_lighting.tooltip": "Controla si los bloques serán iluminados y sombreados suavemente. Esto aumenta ligeramente el tiempo necesario para reconstruir un trozo, pero no afecta a la velocidad de fotogramas.", + "sodium.options.biome_blend.value": "%s bloque(s)", + "sodium.options.biome_blend.tooltip": "Controla el rango que los biomas serán muestreados para la coloración de los bloques. Los valores más altos aumentan en gran medida la cantidad de tiempo que se necesita para construir trozos para disminuir las mejoras en la calidad.", + "sodium.options.entity_distance.tooltip": "Controla la distancia a la que se pueden renderizar las entidades desde el jugador. Los valores más altos aumentan la distancia de renderizado a expensas de la velocidad de fotogramas.", + "sodium.options.entity_shadows.tooltip": "Si está activada, las sombras básicas se mostrarán debajo de los monstruos y otras entidades.", + "sodium.options.vignette.name": "Viñeta", + "sodium.options.vignette.tooltip": "Si se habilita, se renderizará un efecto de viñeta en la vista del jugador. Es muy poco probable que esto suponga una diferencia en la tasa de fotogramas, a menos que la tasa de llenado sea limitada.", + "sodium.options.mipmap_levels.tooltip": "Controla el número de mipmaps que se utilizarán para las texturas del modelo de bloques. Los valores más altos proporcionan un mejor renderizado de los bloques en la distancia, pero pueden afectar negativamente al rendimiento con muchas texturas animadas.", + "sodium.options.use_block_face_culling.name": "Usar eliminación selectiva de caras de bloques", + "sodium.options.use_block_face_culling.tooltip": "Si se activa, sólo se enviarán a renderizar las caras de los bloques que estén orientadas hacia la cámara. Esto puede eliminar un gran número de caras de bloques muy pronto en el proceso de renderizado, ahorrando ancho de banda de memoria y tiempo en la GPU. Algunos paquetes de recursos pueden tener problemas con esta opción, así que prueba a desactivarla si ves agujeros en los bloques.", + "sodium.options.use_fog_occlusion.name": "Usar Oclusión de Niebla", + "sodium.options.use_fog_occlusion.tooltip": "Si se activa, los trozos que se determinan como totalmente ocultos por los efectos de niebla no se renderizarán, lo que ayuda a mejorar el rendimiento. La mejora puede ser más dramática cuando los efectos de niebla son más pesados (como cuando se está bajo el agua), pero puede causar artefactos visuales indeseables entre el cielo y la niebla en algunos escenarios.", + "sodium.options.use_entity_culling.name": "Usar Eliminación de Entidades", + "sodium.options.use_entity_culling.tooltip": "Si se activa, las entidades que se determinen que no están en ningún trozo visible se omitirán durante el renderizado. Esto puede ayudar a mejorar el rendimiento al evitar la renderización de las entidades situadas bajo tierra o detrás de las paredes.", + "sodium.options.use_particle_culling.name": "Utilice la Eliminación de Partículas", + "sodium.options.use_particle_culling.tooltip": "Si se activa, sólo se renderizarán las partículas que se determinen como visibles. Esto puede proporcionar una mejora significativa en la velocidad de fotogramas cuando hay muchas partículas cerca.", + "sodium.options.animate_only_visible_textures.name": "Animar Sólo las Texturas Visibles", + "sodium.options.animate_only_visible_textures.tooltip": "Si se activa, sólo se actualizarán las texturas animadas que sean visibles. Esto puede proporcionar un aumento significativo de la velocidad de fotogramas en algunos equipos, especialmente con paquetes de recursos más pesados. Si tienes problemas con algunas texturas que no se animan, prueba a desactivar esta opción.", + "sodium.options.cpu_render_ahead_limit.name": "Límite de Procesamiento Anticipado de CPU", + "sodium.options.cpu_render_ahead_limit.tooltip": "Especifica el número máximo de fotogramas que la CPU puede esperar a que la GPU termine de renderizar. Los valores muy bajos o altos pueden crear inestabilidad en la tasa de fotogramas.", + "sodium.options.cpu_render_ahead_limit.value": "%s fotograma(s)", + "sodium.options.allow_direct_memory_access.name": "Permitir Aacceso Directo a la Memoria", + "sodium.options.allow_direct_memory_access.tooltip": "Si se activa, se permitirá que algunas rutas de código críticas utilicen el acceso directo a la memoria para mejorar el rendimiento. Esto suele reducir en gran medida la sobrecarga de la CPU para el renderizado de trozos y entidades, pero puede dificultar el diagnóstico de algunos fallos y caídas. Sólo deberías desactivar esto si te lo han pedido o si sabes lo que estás haciendo.", + "sodium.options.enable_memory_tracing.name": "Habilitar Seguimiento de Memoria", + "sodium.options.enable_memory_tracing.tooltip": "Función de depuración. Si está activada, se recogerán los rastros de pila junto con las asignaciones de memoria para ayudar a mejorar la información de diagnóstico cuando se detecten fugas de memoria.", + "sodium.options.performance_impact_string": "Impacto en el Rendimiento: %s", + "sodium.options.use_persistent_mapping.name": "Utilizar el Mapeo Persistente", + "sodium.options.use_persistent_mapping.tooltip": "Si se activa, una pequeña cantidad de memoria será mapeada de forma persistente para acelerar las actualizaciones de chunk y el renderizado de entidades.\n\nRequiere OpenGL 4.4 o ARB_buffer_storage.", + "sodium.options.chunk_memory_allocator.name": "Asignador de Memoria de Chunks", + "sodium.options.chunk_memory_allocator.tooltip": "Selecciona el asignador de memoria que se utilizará para el renderizado de los trozos.\n- ASYNC: Opción más rápida, funciona bien con la mayoría de los controladores gráficos modernos.\n- SWAP: Opción de reserva para los controladores gráficos más antiguos. Puede aumentar significativamente el uso de la memoria.", + "sodium.options.chunk_memory_allocator.async": "Async", + "sodium.options.chunk_memory_allocator.swap": "Intercambiar", + "sodium.options.chunk_update_threads.name": "Subprocesos de Actualización de Chunks", + "sodium.options.chunk_update_threads.tooltip": "Especifica el número de hilos que se van a utilizar para la construcción de los trozos. El uso de más hilos puede acelerar la carga de trozos y la velocidad de actualización, pero puede afectar negativamente a los tiempos de fotogramas.", + "sodium.options.always_defer_chunk_updates.name": "Aplazar Siempre las Actualizaciones de Chunks", + "sodium.options.always_defer_chunk_updates.tooltip": "Si se activa, el renderizado nunca esperará a que terminen las actualizaciones de los chunk, incluso si son importantes. Esto puede mejorar mucho la velocidad de fotogramas en algunos escenarios, pero puede crear un retraso visual significativo en el mundo.", + "sodium.options.buttons.undo": "Deshacer", + "sodium.options.buttons.apply": "Aplicar", + "sodium.options.buttons.donate": "¡Compra café para JellySquid!" +} diff --git a/src/main/resources/assets/sodium/lang/es_mx.json b/src/main/resources/assets/sodium/lang/es_mx.json new file mode 100644 index 000000000..a6847a8a5 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/es_mx.json @@ -0,0 +1,66 @@ +{ + "sodium.option_impact.low": "Bajo", + "sodium.option_impact.medium": "Medio", + "sodium.option_impact.high": "Alto", + "sodium.option_impact.extreme": "Extremo", + "sodium.option_impact.varies": "Varía", + "sodium.options.pages.quality": "Calidad", + "sodium.options.pages.performance": "Rendimiento", + "sodium.options.pages.advanced": "Avanzado", + "sodium.options.view_distance.tooltip": "La distancia de renderizado controla la distancia a la que se renderizará el terreno. Las distancias más cortas significan que se renderizará menos terreno, lo que mejora la velocidad de fotogramas.", + "sodium.options.simulation_distance.tooltip": "La distancia de simulación controla la distancia a la que se cargarán y marcarán el terreno y las entidades. Las distancias más cortas pueden reducir la carga del servidor interno y pueden mejorar la velocidad de fotogramas.", + "sodium.options.brightness.tooltip": "Controla el brillo (gamma) del juego.", + "sodium.options.gui_scale.tooltip": "Establece el factor de escala máximo que se utilizará para la interfaz de usuario. Si se utiliza 'auto', siempre se utilizará el factor de escala más grande.", + "sodium.options.fullscreen.tooltip": "Si se activa, el juego se mostrará en pantalla completa (si es compatible).", + "sodium.options.v_sync.tooltip": "Si se activa, la velocidad de fotogramas del juego se sincronizará con la frecuencia de refresco del monitor, lo que hará que la experiencia sea más fluida a expensas de la latencia de entrada general. Este ajuste puede reducir el rendimiento si tu sistema es demasiado lento.", + "sodium.options.fps_limit.tooltip": "Limita el número máximo de fotogramas por segundo. Esto puede ayudar a reducir el uso de la batería y la carga general del sistema cuando se realiza una multitarea. Si V-Sync está activado, esta opción se ignorará a menos que sea inferior a la frecuencia de refresco de tu pantalla.", + "sodium.options.view_bobbing.tooltip": "Si está activada, la vista del jugador se balanceará y oscilará cuando se mueva. Los jugadores que experimenten mareos al jugar pueden beneficiarse de desactivar esta opción.", + "sodium.options.attack_indicator.tooltip": "Controla dónde se muestra el indicador de ataque en la pantalla.", + "sodium.options.autosave_indicator.tooltip": "Si se activa, se mostrará un indicador cuando el juego esté guardando el mundo en el disco.", + "sodium.options.graphics_quality.tooltip": "La calidad gráfica por defecto controla algunas opciones heredadas y es necesaria para la compatibilidad de los mods. Si las opciones de abajo se dejan en \"Default\", utilizarán esta configuración.", + "sodium.options.clouds_quality.tooltip": "Controla la calidad de las nubes renderizadas en el cielo.", + "sodium.options.weather_quality.tooltip": "Controla la calidad de los efectos de lluvia y nieve.", + "sodium.options.leaves_quality.name": "Calidad de las Hojas", + "sodium.options.leaves_quality.tooltip": "Controla la calidad de las hojas.", + "sodium.options.particle_quality.name": "Calidad de las Partículas", + "sodium.options.particle_quality.tooltip": "Controla el número máximo de partículas que pueden estar presentes en la pantalla en un momento dado.", + "sodium.options.smooth_lighting.tooltip": "Controla si los bloques serán iluminados y sombreados suavemente. Esto aumenta ligeramente el tiempo necesario para reconstruir un trozo, pero no afecta a la velocidad de fotogramas.", + "sodium.options.biome_blend.value": "%s bloque(s)", + "sodium.options.biome_blend.tooltip": "Controla el rango que los biomas serán muestreados para la coloración de los bloques. Los valores más altos aumentan en gran medida la cantidad de tiempo que se necesita para construir trozos para disminuir las mejoras en la calidad.", + "sodium.options.entity_distance.tooltip": "Controla la distancia a la que se pueden renderizar las entidades desde el jugador. Los valores más altos aumentan la distancia de renderizado a expensas de la velocidad de fotogramas.", + "sodium.options.entity_shadows.tooltip": "Si está activada, las sombras básicas se mostrarán debajo de los monstruos y otras entidades.", + "sodium.options.vignette.name": "Viñeta", + "sodium.options.vignette.tooltip": "Si se habilita, se renderizará un efecto de viñeta en la vista del jugador. Es muy poco probable que esto suponga una diferencia en la tasa de fotogramas, a menos que la tasa de llenado sea limitada.", + "sodium.options.mipmap_levels.tooltip": "Controla el número de mipmaps que se utilizarán para las texturas del modelo de bloques. Los valores más altos proporcionan un mejor renderizado de los bloques en la distancia, pero pueden afectar negativamente al rendimiento con muchas texturas animadas.", + "sodium.options.use_block_face_culling.name": "Usar eliminación selectiva de caras de bloques", + "sodium.options.use_block_face_culling.tooltip": "Si se activa, sólo se enviarán a renderizar las caras de los bloques que estén orientadas hacia la cámara. Esto puede eliminar un gran número de caras de bloques muy pronto en el proceso de renderizado, ahorrando ancho de banda de memoria y tiempo en la GPU. Algunos paquetes de recursos pueden tener problemas con esta opción, así que prueba a desactivarla si ves agujeros en los bloques.", + "sodium.options.use_fog_occlusion.name": "Usar Oclusión de Niebla", + "sodium.options.use_fog_occlusion.tooltip": "Si se activa, los trozos que se determinan como totalmente ocultos por los efectos de niebla no se renderizarán, lo que ayuda a mejorar el rendimiento. La mejora puede ser más dramática cuando los efectos de niebla son más pesados (como cuando se está bajo el agua), pero puede causar artefactos visuales indeseables entre el cielo y la niebla en algunos escenarios.", + "sodium.options.use_entity_culling.name": "Usar Eliminación de Entidades", + "sodium.options.use_entity_culling.tooltip": "Si se activa, las entidades que se determinen que no están en ningún trozo visible se omitirán durante el renderizado. Esto puede ayudar a mejorar el rendimiento al evitar la renderización de las entidades situadas bajo tierra o detrás de las paredes.", + "sodium.options.use_particle_culling.name": "Utilice la Eliminación de Partículas", + "sodium.options.use_particle_culling.tooltip": "Si se activa, sólo se renderizarán las partículas que se determinen como visibles. Esto puede proporcionar una mejora significativa en la velocidad de fotogramas cuando hay muchas partículas cerca.", + "sodium.options.animate_only_visible_textures.name": "Animar Sólo las Texturas Visibles", + "sodium.options.animate_only_visible_textures.tooltip": "Si se activa, sólo se actualizarán las texturas animadas que sean visibles. Esto puede proporcionar un aumento significativo de la velocidad de fotogramas en algunos equipos, especialmente con paquetes de recursos más pesados. Si tienes problemas con algunas texturas que no se animan, prueba a desactivar esta opción.", + "sodium.options.cpu_render_ahead_limit.name": "Límite de Procesamiento Anticipado de CPU", + "sodium.options.cpu_render_ahead_limit.tooltip": "Especifica el número máximo de fotogramas que la CPU puede esperar a que la GPU termine de renderizar. Los valores muy bajos o altos pueden crear inestabilidad en la tasa de fotogramas.", + "sodium.options.cpu_render_ahead_limit.value": "%s fotograma(s)", + "sodium.options.allow_direct_memory_access.name": "Permitir Aacceso Directo a la Memoria", + "sodium.options.allow_direct_memory_access.tooltip": "Si se activa, se permitirá que algunas rutas de código críticas utilicen el acceso directo a la memoria para mejorar el rendimiento. Esto suele reducir en gran medida la sobrecarga de la CPU para el renderizado de trozos y entidades, pero puede dificultar el diagnóstico de algunos fallos y caídas. Sólo deberías desactivar esto si te lo han pedido o si sabes lo que estás haciendo.", + "sodium.options.enable_memory_tracing.name": "Habilitar Seguimiento de Memoria", + "sodium.options.enable_memory_tracing.tooltip": "Función de depuración. Si está activada, se recogerán los rastros de pila junto con las asignaciones de memoria para ayudar a mejorar la información de diagnóstico cuando se detecten fugas de memoria.", + "sodium.options.performance_impact_string": "Impacto en el Rendimiento: %s", + "sodium.options.use_persistent_mapping.name": "Utilizar el Mapeo Persistente", + "sodium.options.use_persistent_mapping.tooltip": "Si se activa, una pequeña cantidad de memoria será mapeada de forma persistente para acelerar las actualizaciones de chunk y el renderizado de entidades.\n\nRequiere OpenGL 4.4 o ARB_buffer_storage.", + "sodium.options.chunk_memory_allocator.name": "Asignador de Memoria de Chunks", + "sodium.options.chunk_memory_allocator.tooltip": "Selecciona el asignador de memoria que se utilizará para el renderizado de los trozos.\n- ASYNC: Opción más rápida, funciona bien con la mayoría de los controladores gráficos modernos.\n- SWAP: Opción de reserva para los controladores gráficos más antiguos. Puede aumentar significativamente el uso de la memoria.", + "sodium.options.chunk_memory_allocator.async": "Async", + "sodium.options.chunk_memory_allocator.swap": "Intercambiar", + "sodium.options.chunk_update_threads.name": "Subprocesos de Actualización de Chunks", + "sodium.options.chunk_update_threads.tooltip": "Especifica el número de hilos que se van a utilizar para la construcción de los trozos. El uso de más hilos puede acelerar la carga de trozos y la velocidad de actualización, pero puede afectar negativamente a los tiempos de fotogramas.", + "sodium.options.always_defer_chunk_updates.name": "Aplazar Siempre las Actualizaciones de Chunks", + "sodium.options.always_defer_chunk_updates.tooltip": "Si se activa, el renderizado nunca esperará a que terminen las actualizaciones de los chunk, incluso si son importantes. Esto puede mejorar mucho la velocidad de fotogramas en algunos escenarios, pero puede crear un retraso visual significativo en el mundo.", + "sodium.options.buttons.undo": "Deshacer", + "sodium.options.buttons.apply": "Aplicar", + "sodium.options.buttons.donate": "¡Compra café para JellySquid!" +} diff --git a/src/main/resources/assets/sodium/lang/ko_kr.json b/src/main/resources/assets/sodium/lang/ko_kr.json new file mode 100644 index 000000000..34a7ddf98 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/ko_kr.json @@ -0,0 +1,68 @@ +{ + "sodium.option_impact.low": "하", + "sodium.option_impact.medium": "중", + "sodium.option_impact.high": "상", + "sodium.option_impact.extreme": "최상", + "sodium.option_impact.varies": "때때로 다름", + "sodium.options.pages.quality": "품질", + "sodium.options.pages.performance": "성능", + "sodium.options.pages.advanced": "고급", + "sodium.options.view_distance.tooltip": "렌더 거리는 지형이 어느 거리에서부터 표시될 지 조절합니다. 짧은 거리일수록 더 적게 표시되어 프레임이 향상됩니다.", + "sodium.options.simulation_distance.tooltip": "시뮬레이션 거리는 지형과 개체가 어느 거리에서부터 로딩되고 작동할 지 조절합니다. 짧은 거리일수록 내부 서버의 부하를 줄이고 프레임이 향상될 수 있습니다.", + "sodium.options.brightness.tooltip": "화면의 밝기(감마)를 조절합니다.", + "sodium.options.clouds.name": "구름", + "sodium.options.clouds.tooltip": "구름 표시 여부를 조절합니다..", + "sodium.options.gui_scale.tooltip": "사용자 인터페이스 크기를 조절합니다. '자동' 사용 시, 항상 가장 큰 크기를 사용합니다.", + "sodium.options.fullscreen.tooltip": "활성화 시, 게임이 전체 화면으로 표시됩니다 (지원되는 경우).", + "sodium.options.v_sync.tooltip": "활성화 시, 게임의 프레임이 모니터의 주사율과 동기화되며, 입력 지연을 대가로 일반적으로 더 부드러운 화면을 제공합니다. 이 설정은 시스템이 좋지 않을 경우 성능 저하를 일으킬 수 있습니다.", + "sodium.options.fps_limit.tooltip": "초당 최대 프레임 수를 제한합니다. 이 설정은 배터리 사용량 및 멀티테스킹 시 시스템 부하를 줄일 수 있습니다. V-Sync 활성화 시, 제한값이 모니터의 주사율보다 높으면 설정이 무시됩니다.", + "sodium.options.view_bobbing.tooltip": "활성화 시, 화면의 움직임이 사실적으로 표시됩니다. 게임 플레이 중 멀미를 경험하면 이 설정을 비활성화함으로써 도움이 될 수 있습니다.", + "sodium.options.attack_indicator.tooltip": "공격 지표를 조절합니다.", + "sodium.options.autosave_indicator.tooltip": "활성화 시, 월드 자동 저장 아이콘이 표시됩니다.", + "sodium.options.graphics_quality.tooltip": "기본 그래픽 설정을 조절하며, 모드 호환성에 영향을 줍니다. 아래 옵션들에 '기본'을 사용할 경우 이 그래픽 설정을 따릅니다.", + "sodium.options.clouds_quality.tooltip": "구름의 품질을 조절합니다.", + "sodium.options.weather_quality.tooltip": "비 및 눈 효과의 품질을 조절합니다.", + "sodium.options.leaves_quality.name": "나뭇잎", + "sodium.options.leaves_quality.tooltip": "나뭇잎의 품질을 조절합니다.", + "sodium.options.particle_quality.name": "입자", + "sodium.options.particle_quality.tooltip": "한 화면에 표시할 수 있는 최대 입자 수를 조절합니다.", + "sodium.options.smooth_lighting.tooltip": "블럭이 부드러운 조명 및 음영 효과를 가질 지 조절합니다. 이 설정은 청크 재생성 시간을 조금 증가시키지만 프레임에 영향을 주지 않습니다.", + "sodium.options.biome_blend.value": "%s 블럭", + "sodium.options.biome_blend.tooltip": "생물 균계 혼합에 따른 블럭 색 혼합 범위를 조절합니다. 설정값이 높을 수록 품질 향상을 위해 청크 생성시간이 크게 늘어납니다.", + "sodium.options.entity_distance.tooltip": "개체가 어느 거리에서부터 플레이어에게 표시될 지 조절합니다. 설정값이 높을 수록 개체 표시 거리가 늘어나고 프레임이 저하됩니다.", + "sodium.options.entity_shadows.tooltip": "활성화 시, 기본 그림자가 몹 및 다른 개체 아래에 표시됩니다.", + "sodium.options.vignette.name": "비네팅", + "sodium.options.vignette.tooltip": "활성화 시, 플레이어의 시야에 비네팅 효과가 표시됩니다. 필레이트 제한이 걸려있지 않는 한 프레임에 영향을 줄 가능성은 매우 낮습니다.", + "sodium.options.mipmap_levels.tooltip": "블럭 모델 텍스쳐에 사용할 밉맵 갯수를 조절합니다. 설정값이 높을 수록 먼 거리에서 더 나은 블럭 표시를 제공하지만, 움직이는 텍스쳐가 많을 경우 성능에 영향을 줄 수 있습니다.", + "sodium.options.use_block_face_culling.name": "블럭 면 컬링 사용", + "sodium.options.use_block_face_culling.tooltip": "활성화 시, 플레이어의 시야에 보이는 블럭 면만 렌더됩니다. 이 설정으로 많은 양의 블럭 면을 렌더링 초기 단계에 줄여서 GPU의 메모리 대역폭과 시간을 줄입니다. 일부 리소스 팩에서 문제가 생길 수 있으므로 블럭에 구멍이 보인다면 비활성화함으로써 도움이 될 수 있습니다.", + "sodium.options.use_fog_occlusion.name": "안개 오클루전 사용", + "sodium.options.use_fog_occlusion.tooltip": "활성화 시, 플레이어의 시야에서 안개에 의해 전혀 보이지 않는 청크는 렌더되지 않으며 성능이 향상됩니다. 안개가 심할 수록 성능 향상이 더 극적으로 나타날 수 있습니다 (물 안속에 상황). 하지만 하늘과 안개 사이에서의 일부 상황에는 예기치 않은 시야 버그의 원인이 될 수 있습니다.", + "sodium.options.use_entity_culling.name": "개체 컬링 사용", + "sodium.options.use_entity_culling.tooltip": "활성화 시, 플레이어의 시야에서 보이지 않는 청크에 위치한 개체들은 렌더되지 않습니다. 이 설정은 땅 속이나 벽 뒤에 위치해 있는 개체들을 렌더링 단계에서 제외함으로써 성능이 향상될 수 있습니다.", + "sodium.options.use_particle_culling.name": "입자 컬링 사용", + "sodium.options.use_particle_culling.tooltip": "활성화 시, 플레이어의 시야에서 보이는 입자만 렌더됩니다. 이 설정은 주변에 입자 수가 많을 경우 프레임을 크게 향상시킬 수 있습니다.", + "sodium.options.animate_only_visible_textures.name": "보이는 텍스쳐만 애니메이션 사용", + "sodium.options.animate_only_visible_textures.tooltip": "활성화 시, 플레이어의 시야에서 보이는 텍스쳐만 애니메이션이 업데이트됩니다. 이 설정은 일부 하드웨어에서 (특히 무거운 리소스팩을 사용할 때) 프레임을 크게 향상시킬 수 있습니다. 일부 텍스쳐 애니메이션이 작동하지 않는 문제가 발생한다면 비활성화함으로써 도움이 될 수 있습니다.", + "sodium.options.cpu_render_ahead_limit.name": "CPU 렌더 제한", + "sodium.options.cpu_render_ahead_limit.tooltip": "렌더링을 완료하기 위해 CPU가 GPU에서 대기할 수 있는 최대 프레임 수를 조절합니다. 매우 낮거나 매우 높은 설정 값은 프레임이 요동칠 수 있습니다.", + "sodium.options.cpu_render_ahead_limit.value": "%s 프레임", + "sodium.options.allow_direct_memory_access.name": "메모리 직접참조 허용", + "sodium.options.allow_direct_memory_access.tooltip": "활성화 시, 성능을 위해 일부 핵심 코드는 메모리 직접참조를 사용할 수 있습니다. 이 설정은 청크 및 개체 렌더링에서의 CPU 오버헤드를 크게 감소시키지만, 일부 버그와 게임 충돌을 진단하기 더 어렵게 만들 수 있습니다. 이 설정은 작업을 요청받은 경우나 기능을 정확히 알고 있는 경우에만 비활성화해야합니다.", + "sodium.options.enable_memory_tracing.name": "메모리 추적", + "sodium.options.enable_memory_tracing.tooltip": "디버그 기능. 활성화 시, 스택에 메모리 할당이 수집되어 메모리 누수가 감지된 경우 진단에 도움을 줄 수 있습니다.", + "sodium.options.performance_impact_string": "성능 영향: %s", + "sodium.options.use_persistent_mapping.name": "영구 매핑 사용", + "sodium.options.use_persistent_mapping.tooltip": "활성화 시, 적은 양의 메모리가 영구적으로 청크 업데이트와 개체 렌더링을 위해 매핑됩니다.\n\nOpenGL 4.4 또는 ARB_buffer_storage가 요구됩니다.", + "sodium.options.chunk_memory_allocator.name": "청크 메모리 관리", + "sodium.options.chunk_memory_allocator.tooltip": "청크 렌더링에 사용될 메모리 할당자를 선택합니다.\n- ASYNC: 가장 빠른 옵션, 대부분의 최신 그래픽 드라이버와 함께 잘 작동합니다.\n- SWAP: 오래된 그래픽 드라이버를 위한 대체 옵션. 메모리 사용량이 크게 증가할 수 있습니다.", + "sodium.options.chunk_memory_allocator.async": "Async", + "sodium.options.chunk_memory_allocator.swap": "Swap", + "sodium.options.chunk_update_threads.name": "청크 업데이트 스레드", + "sodium.options.chunk_update_threads.tooltip": "청크 업데이트에 사용할 스레드 갯수를 지정합니다. 더 많은 스레드를 사용하면 청크 로딩과 업데이트 속도를 향상시킬 수 있지만 프레임 시간에 부정적인 영향을 줄 수 있습니다.", + "sodium.options.always_defer_chunk_updates.name": "항상 청크 업데이트 미루기", + "sodium.options.always_defer_chunk_updates.tooltip": "활성화 시, 렌더링은 청크 업데이트가 아무리 중요하더라도 그것이 끝나기까지 기다리지 않습니다. 일부 상황에서 프레임을 크게 향상시킬 수 있지만 월드에 상당한 시야 랙을 발생시킬 수 있습니다.", + "sodium.options.buttons.undo": "취소", + "sodium.options.buttons.apply": "완료", + "sodium.options.buttons.donate": "개발자 후원하기" +} \ No newline at end of file diff --git a/src/main/resources/assets/sodium/lang/pt_br.json b/src/main/resources/assets/sodium/lang/pt_br.json new file mode 100644 index 000000000..04b7e7630 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/pt_br.json @@ -0,0 +1,66 @@ +{ + "sodium.option_impact.low": "Baixo", + "sodium.option_impact.medium": "Médio", + "sodium.option_impact.high": "Alto", + "sodium.option_impact.extreme": "Extremo", + "sodium.option_impact.varies": "Variável", + "sodium.options.pages.quality": "Qualidade", + "sodium.options.pages.performance": "Desempenho", + "sodium.options.pages.advanced": "Opções Avançadas", + "sodium.options.view_distance.tooltip": "A distância de processamento visual controla o quão longe o terreno será carregado. Distâncias menores significam será carregado menos terreno, o que aumenta a taxa de quadros.", + "sodium.options.simulation_distance.tooltip": "A distância de simulação controla o quão longe o terreno e entiades serão simulados e automatizados. Distâncias menores exigem menos dos servidores, o que aumenta a taxa de quadros.", + "sodium.options.brightness.tooltip": "Controla o quão claro o jogo é. Aumentar o brilho pode ajudar pessoas com monitores muito reflexivos ou que estão em ambientes muito claros", + "sodium.options.gui_scale.tooltip": "Define a escala máxima da interface. Se \"auto\" for usado, a maior escala será usada.", + "sodium.options.fullscreen.tooltip": "Caso habilitado e suportado, o jogo será exibido em tela cheia.", + "sodium.options.v_sync.tooltip": "Caso habilitado, a taxa de quadros sera sincronizada com a taxa de atualização do monitor, tornando a experiência visual mais confortável ao custo de um certo atraso visual pouco perceptível. Esta opção pode reduzir o desempenho caso seu computador seja lento.", + "sodium.options.fps_limit.tooltip": "Limita o número máximo de quadros por segundo. Isto pode ajudar a reduzir o consumo de bateria e o carregamento do sistema quando executa multi-tarefas. Se a sincronização vertical estiver ligada, esta opção será ignorada, a menos que seja menor que a taxa de atualização do seu monitor", + "sodium.options.view_bobbing.tooltip": "Caso habilitado, a câmera do jogador irá se balançar quando se movimentar. Jogadores sucetíveis a enjôos podem se beneficiar ao desabilitar esta opção.", + "sodium.options.attack_indicator.tooltip": "Controla onde o indicador de ataque aparece na tela.", + "sodium.options.autosave_indicator.tooltip": "Caso habilitado, um indicador será mostrado quando o jogo estiver sendo salvo.", + "sodium.options.graphics_quality.tooltip": "A qualidade gráfica padrão controla algumas opções de legado e é necessária para a melhor compatibilidade com mods. Caso as opções abaixo estejam como \"Padrão\", elas usarão esta configuração.", + "sodium.options.clouds_quality.tooltip": "Controla a qualidade das nuvens no céu. Elas podem ser planas ou possuírem três dimensões.", + "sodium.options.weather_quality.tooltip": "Controla a qualidade e quantidade de partículas de chuva e neve.", + "sodium.options.leaves_quality.name": "Qualidade das folhas", + "sodium.options.leaves_quality.tooltip": "Controla a qualidade das folhas.", + "sodium.options.particle_quality.name": "Qualidade das partículas", + "sodium.options.particle_quality.tooltip": "Controla o número máximo de partículas que aparecem na tela ao mesmo tempo.", + "sodium.options.smooth_lighting.tooltip": "Controla o quão suave é a iluminação e sombreamento nos blocos. Isso aumenta o tempo necessário para reconstruir uma região, mas não afeta a taxa de quadros.", + "sodium.options.biome_blend.value": "%s bloco(s)", + "sodium.options.biome_blend.tooltip": "Controla o alcance do limiar de cores do terreno e folhagem dos biomas. Valores altos aumentam significativamente o tempo de geração do mundo para diminuir as melhorias na qualidade.", + "sodium.options.entity_distance.tooltip": "Controla o quão longe as criaturas podem ser processadas visualmente. Valores altos aumentam a distância de processamento visual ao custo de uma diminuição da taxa de quadros.", + "sodium.options.entity_shadows.tooltip": "Caso habilitado, uma sombra projetada bem simples será mostrada abaixo de riaturas e outras entidades, inclusive jogadores.", + "sodium.options.vignette.name": "Vinheta", + "sodium.options.vignette.tooltip": "Caso habilitada, um efeito de escurecimento das bordas da tela será aplicado na câmera em primeira pessoa do jogador. É muitíssimo pouco provável que este efeito possua algum efeito na taxa de quadros, a menos que o computador possua uma taxa de preenchimento limitada.", + "sodium.options.mipmap_levels.tooltip": "Controla o a filtragem de texturas de blocos a uma certa distância. Valores altos proporcionam uma qualidade melhor dos blocos que estão muito distantes, mas afeta o desempenho se houverem muitos blocos animados.", + "sodium.options.use_block_face_culling.name": "Utilizar abate de face de bloco", + "sodium.options.use_block_face_culling.tooltip": "Caso habilitado, apenas a lateral dos blocos que estão viradas para a câmera serão processadas visualmente. Isto elimina um número imenso de faces de blocos no estágio de carregamento, salvando memória e processamento visual da placa de vídeo. Alguns pacotes de recursos podem ter problemas com esta opção, então tente desativar esta opção caso esteja vendo falhas em blocos.", + "sodium.options.use_fog_occlusion.name": "Utilizar oclusão de neblina", + "sodium.options.use_fog_occlusion.tooltip": "Caso habilitado, regiões que devem estar completamente escondidas pela neblina não serão carregadas para favorecer o desempenho. O aumento no desempenho pode ser ainda mais evidente quando os efeitos de neblina estão mais intensos, como quando se está debaixo d'água, mas, em alguns casos, pode causar falhas de processamento visual de terreno indesejáveis.", + "sodium.options.use_entity_culling.name": "Utilizar abate de entidades", + "sodium.options.use_entity_culling.tooltip": "Caso habilitado, entidades que não devem estar visíveis, como atrás de blocos ou debaixo da terra, por exemplo, serão ignoradas no processamento visual, o que aumenta o desempenho.", + "sodium.options.use_particle_culling.name": "Utilizar abate de partículas", + "sodium.options.use_particle_culling.tooltip": "Caso habilitado, apenas partículas que devem estar visíveis serão processadas visualmente. Isto pode aumenta o desempenho quando se está em locais com muitas partículas.", + "sodium.options.animate_only_visible_textures.name": "Animar apenas texturas visíveis", + "sodium.options.animate_only_visible_textures.tooltip": "Caso habilitado, apenas texturas animadas visíveis serão processadas visualmente. Isto pode aumentas significativamente o desempenho em alguns computadores, especialmente quando se usa muitos pacotes de recursos. Caso esteja experienciando falhas na animação em texturas, desabilite esta opção.", + "sodium.options.cpu_render_ahead_limit.name": "Limite de processamento visual adiantado do processador", + "sodium.options.cpu_render_ahead_limit.tooltip": "Especifica o número máximo de quadros que o processador pode esperar a placa de vídeo terminar de processar. Números muito baixos ou altos podem criar instabilidade na taxa de quadros.", + "sodium.options.cpu_render_ahead_limit.value": "%s quadro(s)", + "sodium.options.allow_direct_memory_access.name": "Permitir acesso direto à memória", + "sodium.options.allow_direct_memory_access.tooltip": "Caso habilitado, alguns caminhos de código críticos poderão ter acesso direto à memória para fins de desempenho. Isto geralmente reduz bastante sobrecargas no processador no que tange o processamento visual de entidades, mas pode ser mais difícil diagnosticar alguns erros ou interrupções (crashes). Você só deve desabilitar esta opção se for requerido ou souber o que está fazendo.", + "sodium.options.enable_memory_tracing.name": "Habilitar rastreio de memória", + "sodium.options.enable_memory_tracing.tooltip": "Recurso de diagnóstico. Caso habilitado, dados de rastreamentos serão coletados junto com as alocações de memória para ajudar a melhorar as informações de diagnóstico quando vazamentos de memória forem detectados .", + "sodium.options.performance_impact_string": "Impacto no desempenho: %s", + "sodium.options.use_persistent_mapping.name": "Utilizar mapeamento de persistência", + "sodium.options.use_persistent_mapping.tooltip": "Caso habilitado, uma pequena quantidade de memória será persistentemente mapeada como um filtro de teste para envio de regiões, ajudando a reduzir sobrecargas do processador e instabilidade de tempo de quadro quando carregando ou atualizando regiões.\n\nRequer OpenGL 4.4 ou ARB_buffer_storage.", + "sodium.options.chunk_memory_allocator.name": "Alocador de memória de regiões", + "sodium.options.chunk_memory_allocator.tooltip": "Seleciona um alocador de memória para ser usado no processamento visual das regiões.\n- ASSÍNCRONO: Opção mais rápida, funcina melhor com placas gráficas mais atuais.\n- ALTERNATE: Opção para máquinas antigas. Pode aumentar o uso da memória significativamente.", + "sodium.options.chunk_memory_allocator.async": "Assíncrono", + "sodium.options.chunk_memory_allocator.swap": "Alternante", + "sodium.options.chunk_update_threads.name": "Tópicos de atualizações de regiões", + "sodium.options.chunk_update_threads.tooltip": "Especifica o número de tópicos a serem usadas no carregamento de regiões. Utilizar mais tópicos pode acelerar o carregamento e atualização de regiões, mas pode impactar negativamente a taxa de quadros.", + "sodium.options.always_defer_chunk_updates.name": "Sempre adiar atualizações de regiões", + "sodium.options.always_defer_chunk_updates.tooltip": "Caso habilitado, o processamento visual nunca vai aguardar o término das atualizações de regiões, mesmo quado são importantes. Isto pode aumentar significativamente a taxa de quadros em alguns casos, mas pode criar vários erros visuais no mundo.", + "sodium.options.buttons.undo": "Desfazer", + "sodium.options.buttons.apply": "Aplicar", + "sodium.options.buttons.donate": "Doações" +} diff --git a/src/main/resources/assets/sodium/lang/ru_ru.json b/src/main/resources/assets/sodium/lang/ru_ru.json new file mode 100644 index 000000000..542a7f151 --- /dev/null +++ b/src/main/resources/assets/sodium/lang/ru_ru.json @@ -0,0 +1,66 @@ +{ + "sodium.option_impact.low": "Низкое", + "sodium.option_impact.medium": "Среднее", + "sodium.option_impact.high": "Высокое", + "sodium.option_impact.extreme": "Огромное", + "sodium.option_impact.varies": "Разное", + "sodium.options.pages.quality": "Качество", + "sodium.options.pages.performance": "Производительность", + "sodium.options.pages.advanced": "Расширенные", + "sodium.options.view_distance.tooltip": "Дальность прорисовки определяет, как далеко будет видна местность. Меньшие значения соответствуют меньшей площади, что улучшает частоту кадров.", + "sodium.options.simulation_distance.tooltip": "Дальность симуляции определяет, как далеко местность и сущности будут загружаться и обрабатываться. Меньшие значения могут уменьшить нагрузку на внутренний сервер и увеличить частоту кадров.", + "sodium.options.brightness.tooltip": "Управляет яркостью (гаммой) игры.", + "sodium.options.gui_scale.tooltip": "Устанавливает коэффициент масштабирования для интерфейса. При выборе «Авто» всегда будет использоваться наибольший размер.", + "sodium.options.fullscreen.tooltip": "Если включено, игра будет открыта во весь экран (если доступно).", + "sodium.options.v_sync.tooltip": "Если включена, частота кадров будет синхронизирована с частотой обновления монитора, обеспечивая более плавную игру за счет небольшой задержки ввода. Может понизить производительность слабых систем.", + "sodium.options.fps_limit.tooltip": "Ограничивает максимальное количество кадров в секунду. Это поможет уменьшить энергопотребление и нагрузку на систему в обеспечении многозадачности. При включённой вертикальной синхронизации этот параметр будет игнорироваться, если он выше частоты обновления вашего экрана.", + "sodium.options.view_bobbing.tooltip": "Если включено, обзор игрока будет покачиваться при движении. Отключение этого параметра может помочь людям, страдающим от укачивания во время игры.", + "sodium.options.attack_indicator.tooltip": "Управляет расположением индикатора атаки на экране.", + "sodium.options.autosave_indicator.tooltip": "Если включено, при сохранении игрой мира на диск будет показываться индикатор.", + "sodium.options.graphics_quality.tooltip": "Качество графики по умолчанию управляет некоторыми зависимыми настройками и необходимо для совместимости с модами. Опции ниже со значением «По умолчанию» будут использовать этот параметр.", + "sodium.options.clouds_quality.tooltip": "Управляет качеством отрисовки облаков в небе.", + "sodium.options.weather_quality.tooltip": "Управляет качеством эффектов дождя и снега.", + "sodium.options.leaves_quality.name": "Качество листьев", + "sodium.options.leaves_quality.tooltip": "Управляет качеством листьев.", + "sodium.options.particle_quality.name": "Качество частиц", + "sodium.options.particle_quality.tooltip": "Управляет максимальным количеством частиц, которые могут одновременно присутствовать на экране.", + "sodium.options.smooth_lighting.tooltip": "Управляет мягким освещением и затенением блоков. Это немного увеличивает время сборки чанка, однако не влияет на частоту кадров.", + "sodium.options.biome_blend.value": "Блоков: %s", + "sodium.options.biome_blend.tooltip": "Управляет диапазоном анализа биомов для расцветки блока. Более высокие значения сильно увеличивают затрачиваемое на сборку чанков время ради незначительного улучшения качества.", + "sodium.options.entity_distance.tooltip": "Управляет дальностью видимости сущностей. Высокие значения увеличивают расстояние отображения за счёт частоты кадров.", + "sodium.options.entity_shadows.tooltip": "Если включено, под мобами и другими сущностями будут отображаться простые тени.", + "sodium.options.vignette.name": "Виньетирование", + "sodium.options.vignette.tooltip": "Если включено, на экране игрока будет отображаться эффект виньетирования. Вряд ли повлияет на частоту кадров, если ваш ГП не ограничен скоростью заполнения пикселей.", + "sodium.options.mipmap_levels.tooltip": "Управляет количеством уровней детализации для текстур блоков. Более высокие значения обеспечивают лучший вид на расстоянии, но могут негативно повлиять на производительность с большим количеством анимированных текстур.", + "sodium.options.use_block_face_culling.name": "Отбраковка поверхностей блоков", + "sodium.options.use_block_face_culling.tooltip": "Если включено, на рендеринг будут выводиться только видимые для камеры поверхности блоков. Это может исключить большое количество поверхностей на ранней стадии отрисовки, экономя пропускную способность памяти и время для ГП. Некоторые наборы ресурсов могут иметь проблемы с этой опцией, поэтому попробуйте отключить её, если вы видите дыры в блоках.", + "sodium.options.use_fog_occlusion.name": "Окклюзия тумана", + "sodium.options.use_fog_occlusion.tooltip": "Если включено, чанки, определённые как полностью скрытые в тумане, не будут отображаться ради повышения производительности. Улучшение может быть более значительным, когда эффект тумана сильнее (например, под водой), но в некоторых сценах это может привести к нежелательным визуальным артефактам между небом и туманом.", + "sodium.options.use_entity_culling.name": "Отбраковка сущностей", + "sodium.options.use_entity_culling.tooltip": "Если включено, сущности, обнаруженные вне видимых чанков, не будут выводиться на рендеринг. Это может помочь улучшить производительность, исключив рендеринг сущностей, находящихся под землёй или за стенами.", + "sodium.options.use_particle_culling.name": "Отбраковка частиц", + "sodium.options.use_particle_culling.tooltip": "Если включено, будут отображаться только те частицы, которые определены как видимые. Это может обеспечить значительное улучшение частоты кадров, когда рядом находится много частиц.", + "sodium.options.animate_only_visible_textures.name": "Анимировать только видимое", + "sodium.options.animate_only_visible_textures.tooltip": "Если включено, будут обновляться лишь видимые анимированные текстуры. Это может значительно повысить частоту кадров на некоторых устройствах, особенно с \"тяжёлыми\" наборами ресурсов. Если у каких-то текстур возникают проблемы с анимацией, попробуйте отключить эту опцию.", + "sodium.options.cpu_render_ahead_limit.name": "Лимит подготовки кадров", + "sodium.options.cpu_render_ahead_limit.tooltip": "Задаёт максимальное количество кадров, которые ЦП может подготавливать для рендеринга на ГП. Очень низкие или высокие значения могут дестабилизировать частоту кадров.", + "sodium.options.cpu_render_ahead_limit.value": "Кадров: %s", + "sodium.options.allow_direct_memory_access.name": "Разрешить прямой доступ к памяти", + "sodium.options.allow_direct_memory_access.tooltip": "Если включено, некоторым важнейшим частям кода будет разрешено использовать прямой доступ к памяти для повышения производительности. Зачастую это сильно снижает накладные расходы на ЦП при рендеринге чанков и сущностей, но может усложнить диагностику некоторых ошибок и вылетов. Отключать это стоит, только если вы следуете инструкциям или точно знаете, что делаете.", + "sodium.options.enable_memory_tracing.name": "Включить трассировку памяти", + "sodium.options.enable_memory_tracing.tooltip": "Функция отладки. Если включено, трассировка стека будет собираться вместе с распределением памяти, чтобы помочь улучшить диагностическую информацию при обнаружении утечек памяти.", + "sodium.options.performance_impact_string": "Влияние на FPS: %s", + "sodium.options.use_persistent_mapping.name": "Постоянный буфер памяти", + "sodium.options.use_persistent_mapping.tooltip": "Если включено, небольшое количество памяти будет постоянно использоваться в качестве промежуточного буфера для загрузки чанков, помогая уменьшить накладные расходы на ЦП и нестабильность времени отрисовки кадра при их загрузке/выгрузке.\n\nТребуется OpenGL 4.4 или ARB_buffer_storage.", + "sodium.options.chunk_memory_allocator.name": "Распределение памяти", + "sodium.options.chunk_memory_allocator.tooltip": "Выбирает распределитель памяти, используемый для рендеринга чанков.\n- ASYNC: Быстрейший вариант, хорошо работает с большинством современных графических драйверов.\n- SWAP: Запасной вариант для старых графических драйверов. Может существенно увеличить использование памяти.", + "sodium.options.chunk_memory_allocator.async": "Async", + "sodium.options.chunk_memory_allocator.swap": "Swap", + "sodium.options.chunk_update_threads.name": "Потоки обновления чанков", + "sodium.options.chunk_update_threads.tooltip": "Задаёт количество потоков для сборки чанков. Использование большего количества потоков способствует повышению скорости обновления и загрузки чанков, но может негативно повлиять на время кадра.", + "sodium.options.always_defer_chunk_updates.name": "Всегда откладывать обновления", + "sodium.options.always_defer_chunk_updates.tooltip": "Если включено, рендеринг никогда не будет ожидать завершения обновлений чанков, даже если они важны. Это способствует сильному увеличению частоты кадров в некоторых ситуациях, но может привести к появлению существенных визуальных ошибок в мире.", + "sodium.options.buttons.undo": "Отмена", + "sodium.options.buttons.apply": "Применить", + "sodium.options.buttons.donate": "Угостите нас кофе!" +} \ No newline at end of file diff --git a/src/main/resources/assets/sodium/lang/zh_cn.json b/src/main/resources/assets/sodium/lang/zh_cn.json new file mode 100644 index 000000000..c5ca2765a --- /dev/null +++ b/src/main/resources/assets/sodium/lang/zh_cn.json @@ -0,0 +1,82 @@ +{ + "sodium.option_impact.low": "低", + "sodium.option_impact.medium": "中", + "sodium.option_impact.high": "高", + "sodium.option_impact.extreme": "极高", + "sodium.option_impact.varies": "视情况而定", + "sodium.options.pages.quality": "品质", + "sodium.options.pages.performance": "性能", + "sodium.options.pages.advanced": "高级", + "sodium.options.view_distance.tooltip": "渲染距离控制地形应被渲染至多远。更低的距离意味着更少的可见地形,也就有利于提升帧率。", + "sodium.options.simulation_distance.tooltip": "模拟距离控制加载事件/实体运算的距离。较短的距离可以减少内置服务端的负载,并可能提高帧率。", + "sodium.options.brightness.tooltip": "控制游戏画面的亮度(伽马值)。", + "sodium.options.clouds.name": "云", + "sodium.options.clouds.tooltip": "控制云是否可见。", + "sodium.options.gui_scale.tooltip": "设置界面尺寸比例。如果设置为“自动”,则使用可能的最大比例。", + "sodium.options.fullscreen.tooltip": "启用后,游戏将全屏显示(若支持)。", + "sodium.options.v_sync.tooltip": "启用后,游戏的帧率将与显示器的刷新率同步,从而在牺牲整体输入延迟的情况下获得更流畅的体验。如果设备配置过低,此设置可能会降低性能。", + "sodium.options.fps_limit.tooltip": "限制最大帧率。此选项有限制游戏渲染速度的的效果,因此开启此选项有利于提升电池续航或多任务处理。启用垂直同步时此选项会被自动忽略,除非该值低于显示器的刷新", + "sodium.options.view_bobbing.tooltip": "启用后,玩家在移动时的视角会摇晃摆动。禁用该选项可缓解晕动症症状。", + "sodium.options.attack_indicator.tooltip": "控制攻击指示器显示在屏幕上的位置。", + "sodium.options.autosave_indicator.tooltip": "启用后,当游戏在保存世界到磁盘时,就会显示一个指示器。", + "sodium.options.graphics_quality.tooltip": "默认图像品质控制一些原版选项,且对于Mod兼容性是必要的。若下方的选项保留为“默认”,则将会使用此选项的品质。", + "sodium.options.clouds_quality.tooltip": "控制云在空中的渲染品质。", + "sodium.options.weather_quality.tooltip": "控制雨、雪的渲染质量。", + "sodium.options.leaves_quality.name": "树叶", + "sodium.options.leaves_quality.tooltip": "控制树叶的渲染品质。", + "sodium.options.particle_quality.name": "粒子", + "sodium.options.particle_quality.tooltip": "控制每次可以在屏幕上出现的粒子的最大数量。", + "sodium.options.smooth_lighting.tooltip": "控制光照的平滑效果\n\n关 - 无平滑光照\n低 - 只平滑方块的光照\n高 (新!) - 平滑方块和实体的光照", + "sodium.options.biome_blend.value": "%s个方块", + "sodium.options.biome_blend.tooltip": "控制生物群系之间方块颜色的采样范围。较高的值会极大地增加渲染区块时因质量改进而所花费的时间。", + "sodium.options.entity_distance.tooltip": "控制实体的显示距离。较高的值会以牺牲帧率为代价增加渲染距离。", + "sodium.options.entity_shadows.tooltip": "启用后,在生物和其他实体下面渲染简单的阴影。", + "sodium.options.vignette.name": "晕影", + "sodium.options.vignette.tooltip": "启用后,屏幕四角处会轻微变暗。除非限制覆盖范围,否则基本不影响帧率。", + "sodium.options.mipmap_levels.tooltip": "控制平滑材质的多级纹理(Mipmap)的级别。较高的级别可使远处的物体获得更好的渲染效果,但可能会在渲染很多动态材质时产生性能损失。", + "sodium.options.use_chunk_multidraw.name": "使用区块多重绘制技术", + "sodium.options.use_chunk_multidraw.tooltip": "多重绘制技术允许使用较少的绘制调用渲染多个区块,大大减少了在渲染世界时的CPU开销,同时还可能更有效地利用GPU。这种优化可能会导致一些显卡驱动程序出现问题,所以如果您遇到故障,可以尝试禁用它。", + "sodium.options.use_vertex_objects.name": "使用顶点数组对象", + "sodium.options.use_vertex_objects.tooltip": "将关于如何渲染顶点数据的信息移动到驱动程序中来提高性能,使其能够更好地优化重复渲染相同对象的情况。通常情况下,除非使用了不兼容的模组,没有禁用它的理由。", + "sodium.options.use_block_face_culling.name": "启用方块表面剔除", + "sodium.options.use_block_face_culling.tooltip": "启用后,将只会渲染面向镜头的方块表面。 这可以在渲染过程的早期剔除大量方块表面,从而节省GPU上的内存带宽和时间。 某些资源包可能会遇到此选项的问题,因此如果你看到方块显示不全,请尝试禁用它。", + "sodium.options.use_compact_vertex_format.name": "使用紧凑型顶点格式", + "sodium.options.use_compact_vertex_format.tooltip": "如果启用,将使用更紧凑的顶点格式来渲染区块。这可以显著减少图形内存使用和带宽需求,尤其是对于集成显卡而言。但是,由于它减少了位置和纹理坐标属性的精度,可能会导致一些资源包出现深度冲突问题。", + "sodium.options.use_fog_occlusion.name": "启用迷雾遮挡", + "sodium.options.use_fog_occlusion.tooltip": "启用后,被迷雾效果完全隐藏的区块将不会被渲染,有助于提高性能。 当迷雾效果较重时(例如在水下时),改进可能会更加显着,但在某些情况下可能会导致天空和雾之间出现不良的视觉伪影。", + "sodium.options.use_entity_culling.name": "启用实体剔除", + "sodium.options.use_entity_culling.tooltip": "启用后,则在渲染期间跳过在不可见区块的实体。 这可以通过避免渲染位于地下或墙后的实体来帮助提高性能。", + "sodium.options.use_particle_culling.name": "启用颗粒剔除", + "sodium.options.use_particle_culling.tooltip": "启用后,将仅渲染可见的颗粒。当周围有许多颗粒时,这可以显著地提高帧率。", + "sodium.options.animate_only_visible_textures.name": "仅渲染可见的动态材质", + "sodium.options.animate_only_visible_textures.tooltip": "启用后,将只会更新可见的动画材质。 这可以显着提高某些低配电脑的帧率。 尤其是对于较大的资源包。因此如果你看到动态材质不生效,请尝试禁用此选项。", + "sodium.options.translucency_sorting.name": "透明度排序", + "sodium.options.translucency_sorting.tooltip": "启用后,透明方块将被正确排序,从后向前渲染,以提供更好的渲染效果,特别是当你的视野中有多个重叠的透明方块时。此操作在性能上相对较昂贵。", + "sodium.options.cpu_render_ahead_limit.name": "CPU提前渲染限制", + "sodium.options.cpu_render_ahead_limit.tooltip": "指定CPU可以在GPU上等待完成渲染的最大帧数。很低或很高的值可能会导致帧速率不稳定。", + "sodium.options.cpu_render_ahead_limit.value": "%s帧", + "sodium.options.allow_direct_memory_access.name": "允许直接访问内存", + "sodium.options.allow_direct_memory_access.tooltip": "启用后,将允许某些关键代码使用路径直接访问内存来提高性能。 这通常会大大降低区块和实体渲染的CPU性能,但会使诊断某些错误和崩溃变得更加困难。 如果你被要求或以其他方式知道您在做什么,你应该只禁用它。", + "sodium.options.ignore_driver_blacklist.name": "忽略驱动程序黑名单", + "sodium.options.ignore_driver_blacklist.tooltip": "如果启用,将忽略与您的硬件/驱动程序配置已知的不兼容性,允许您启用可能会导致游戏问题的选项。通常情况下,除非您完全知道自己在做什么,否则不应该触碰这个选项。在更改此选项后,您必须保存、关闭,然后重新打开设置屏幕。", + "sodium.options.enable_memory_tracing.name": "启用内存跟踪", + "sodium.options.enable_memory_tracing.tooltip": "调试功能。 启用后,堆栈追踪将与内存分配一起收集,以帮助在检测到内存泄漏时改进诊断信息。", + "sodium.options.performance_impact_string": "性能影响:%s", + "sodium.options.use_persistent_mapping.name": "启用持久映射区域", + "sodium.options.use_persistent_mapping.tooltip": "启用后, 将少量内存一直作为区块刷新的映射缓冲区,有助于减少加载或更新区块时的CPU占用及帧率不稳定。\n\n需要OpenGL4.4或ARB_buffer_storge。", + "sodium.options.chunk_memory_allocator.name": "区块内存分配器", + "sodium.options.chunk_memory_allocator.tooltip": "选择区块渲染的内存分配器。\n-异步:最快的选项,适合大多数现代图形显示驱动。\n-交换:旧图形显示驱动的备用选项。可能会显著增加内存占用。", + "sodium.options.chunk_memory_allocator.async": "异步", + "sodium.options.chunk_memory_allocator.swap": "交换", + "sodium.options.chunk_update_threads.name": "区块更新线程数", + "sodium.options.chunk_update_threads.tooltip": "指定用于区块构建的线程数。使用更多线程可以提高区块加载/更新速度,但可能会对帧速率有负面影响。", + "sodium.options.always_defer_chunk_updates.name": "延迟区块更新", + "sodium.options.always_defer_chunk_updates.tooltip": "启用后,渲染将不会等待区块更新完成,即使它们很重要。这可以在某些场景中大大提升帧速率,但可能会在世界中造成显著的视觉滞后", + "sodium.options.use_no_error_context.name": "使用无错误上下文", + "sodium.options.use_no_error_context.tooltip": "如果启用,OpenGL上下文将被创建时禁用错误检查。这可能会略微提高性能,但也增加了游戏崩溃而不是优雅处理OpenGL错误的风险。如果您遇到无法解释的突发崩溃,应禁用此选项。", + "sodium.options.threads.value": "%s 线程", + "sodium.options.default": "默认", + "sodium.options.buttons.undo": "撤销", + "sodium.options.buttons.apply": "应用", + "sodium.options.buttons.donate": "给JellySquid买杯咖啡!" +} diff --git a/src/main/resources/assets/sodium/shaders/chunk_gl20.f.glsl b/src/main/resources/assets/sodium/shaders/chunk_gl20.f.glsl new file mode 100644 index 000000000..c9649f24c --- /dev/null +++ b/src/main/resources/assets/sodium/shaders/chunk_gl20.f.glsl @@ -0,0 +1,54 @@ +#version 110 + +varying vec4 v_Color; // The interpolated vertex color +varying vec2 v_TexCoord; // The interpolated block texture coordinates +varying vec2 v_LightCoord; // The interpolated light map texture coordinates + +uniform sampler2D u_BlockTex; // The block texture sampler +uniform sampler2D u_LightTex; // The light map texture sampler + +#ifdef USE_FOG +varying float v_FragDistance; +uniform vec4 u_FogColor; // The color of the fog +#endif + +#ifdef USE_FOG_EXP2 +uniform float u_FogDensity; + +// e^(-density * c^2) +float getFogFactor() { + float dist = v_FragDistance * u_FogDensity; + return 1.0 / exp2(dist * dist); +} +#endif + +#ifdef USE_FOG_LINEAR +uniform float u_FogLength; // FogStart - FogEnd +uniform float u_FogEnd; + +// (end - dist) / (end - start) +float getFogFactor() { + return (u_FogEnd - v_FragDistance) / u_FogLength; +} +#endif + +void main() { + // Block texture sample + vec4 sampleBlockTex = texture2D(u_BlockTex, v_TexCoord); + + // Light map texture sample + vec4 sampleLightTex = texture2D(u_LightTex, v_LightCoord); + + // Blend the colors from both textures and the vertex itself + vec4 diffuseColor = sampleBlockTex * sampleLightTex * v_Color; + +#ifdef USE_FOG + float fogFactor = clamp(getFogFactor(), 0.0, 1.0); + + gl_FragColor = mix(u_FogColor, diffuseColor, fogFactor); + gl_FragColor.a = diffuseColor.a; +#else + // No fog is being used, so the fragment color is just that of the blended texture color + gl_FragColor = diffuseColor; +#endif +} diff --git a/src/main/resources/assets/sodium/shaders/chunk_gl20.v.glsl b/src/main/resources/assets/sodium/shaders/chunk_gl20.v.glsl new file mode 100644 index 000000000..9732b17b6 --- /dev/null +++ b/src/main/resources/assets/sodium/shaders/chunk_gl20.v.glsl @@ -0,0 +1,40 @@ +#version 110 +attribute vec3 a_Pos; // The position of the vertex +attribute vec4 a_Color; // The color of the vertex +attribute vec2 a_TexCoord; // The block texture coordinate of the vertex +attribute vec2 a_LightCoord; // The light map texture coordinate of the vertex + +varying vec4 v_Color; +varying vec2 v_TexCoord; +varying vec2 v_LightCoord; + +#ifdef USE_FOG +varying float v_FragDistance; +#endif + +uniform mat4 u_ModelViewProjectionMatrix; +uniform vec3 u_ModelScale; +uniform vec2 u_TextureScale; + +// The model translation for this draw call. +attribute vec4 d_ModelOffset; + +void main() { + // Translates the vertex position around the position of the camera + // This can be used to calculate the distance of the vertex from the camera without needing to + // transform it into model-view space with a matrix, which is much slower. + vec3 pos = (a_Pos * u_ModelScale) + d_ModelOffset.xyz; + +#ifdef USE_FOG + v_FragDistance = length(pos); +#endif + + // Transform the vertex position into model-view-projection space + gl_Position = u_ModelViewProjectionMatrix * vec4(pos, 1.0); + + // Pass the color and texture coordinates to the fragment shader + v_Color = a_Color; + v_TexCoord = a_TexCoord * u_TextureScale; + v_LightCoord = a_LightCoord; +} + diff --git a/src/main/resources/assets/sodium/textures/gui/arrows.png b/src/main/resources/assets/sodium/textures/gui/arrows.png new file mode 100644 index 000000000..01161492c Binary files /dev/null and b/src/main/resources/assets/sodium/textures/gui/arrows.png differ diff --git a/src/main/resources/centerDepth.fsh b/src/main/resources/centerDepth.fsh new file mode 100644 index 000000000..edf76e80a --- /dev/null +++ b/src/main/resources/centerDepth.fsh @@ -0,0 +1,32 @@ +#version VERSIONPLACEHOLDER + +// This will be removed by Iris if the system does not support GL3. +#define IS_GL3 + +uniform sampler2D depth; +uniform sampler2D altDepth; +uniform float lastFrameTime; +uniform float decay; + +#ifdef IS_GL3 +out float oculus_fragColor; +#endif + +void main() { + float currentDepth = texture2D(depth, vec2(0.5)).r; + float decay2 = 1.0 - exp(-decay * lastFrameTime); + float oldDepth = texture2D(altDepth, vec2(0.5)).r; + + #ifdef IS_GL3 + if (isnan(oldDepth)) { + oldDepth = currentDepth; + } + + oculus_fragColor = mix(oldDepth, currentDepth, decay2); + #else + if (oldDepth != oldDepth) { // cheap isNaN + oldDepth = currentDepth; + } + gl_FragColor = vec4(mix(texture2D(altDepth, vec2(0.5)).r, currentDepth, decay2), 0, 0, 0); + #endif +} \ No newline at end of file diff --git a/src/main/resources/centerDepth.vsh b/src/main/resources/centerDepth.vsh new file mode 100644 index 000000000..ce81abb06 --- /dev/null +++ b/src/main/resources/centerDepth.vsh @@ -0,0 +1,2 @@ +#version 120 +void main() { gl_Position = ftransform(); } diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index b14cd4c88..21ceb2475 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,17 +1,62 @@ -[ { - "modid": "${modId}", - "name": "${modName}", - "description": "The little angle that supports shaders while saving your FPS from a certain death", - "version": "${modVersion}", - "mcversion": "1.7.10", - "url": "", - "updateUrl": "", - "authors": ["eigenraven","vlaetansky","karyonix","sonic ether","id_miner","daxnitro"], - "credits": "Based on daxnitro's GLSL Shaders Mod.", - "logoFile": "", - "screenshots": [], - "parent":"", - "dependencies": [] + "modListVersion": 2, + "modList": [ + { + "modid": "${modId}", + "name": "${modName}", + "description": "The little angle that supports shaders while saving your FPS from a certain death", + "version": "${modVersion}", + "mcversion": "1.7.10", + "url": "https://github.com/GTNewHorizons/angelica", + "updateUrl": "", + "authors": [ + "eigenraven", + "mitchej123", + "glowredman", + "caedis", + "vlaetansky", + "karyonix", + "sonic ether", + "id_miner", + "daxnitro" + ], + "credits": "Based on daxnitro's GLSL Shaders Mod. Includes backported code from Oculus and Iris", + "logoFile": "", + "screenshots": [], + "parent": "", + "dependencies": [] + }, + { + "modid": "notfine", + "name": "NotFine", + "description": "Extra video settings for Minecraft 1.7.10 implemented with Mixins", + "version": "${modVersion}", + "mcversion": "${minecraftVersion}", + "url": "https://github.com/jss2a98aj/NotFine", + "updateUrl": "", + "authorList": [ + "jss2a98aj" + ], + "credits": "", + "logoFile": "", + "screenshots": [], + "parent": "", + "requiredMods": [], + "dependencies": [], + "dependants": [], + "useDependencyInformation": true + }, + { + "modid": "embeddium", + "name": "Embeddium", + "version": "${modVersion}", + "mcversion": "${minecraftVersion}", + "authorList": [ + "embeddedt", + "NanoLive", + "CaffeineMC" + ] + } + ] } -] + diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..1c6bd064a --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 1, + "description": "Angelica resources" + } +} diff --git a/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/double_plant_grass_bottom.png b/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/double_plant_grass_bottom.png new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/double_plant_grass_top.png b/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/double_plant_grass_top.png new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/tallgrass.png b/src/main/resources/resourcepacks/vanilla_overrides/assets/minecraft/textures/blocks/tallgrass.png new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/rubidium.mixins.json b/src/main/resources/rubidium.mixins.json new file mode 100644 index 000000000..e18ac878c --- /dev/null +++ b/src/main/resources/rubidium.mixins.json @@ -0,0 +1,75 @@ +{ + "package": "me.jellysquid.mods.sodium.mixin", + "required": true, + "minVersion": "0.0.0", + "compatibilityLevel": "JAVA_8", + "plugin": "me.jellysquid.mods.sodium.mixin.SodiumMixinPlugin", + "refmap": "embeddium-refmap.json", + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "conformVisibility": true + }, + "client": [ + "core.MixinDirection", + "core.frustum.MixinFrustum", + "core.matrix.MixinMatrix3f", + "core.matrix.MixinMatrix4f", + "core.matrix.MixinGameRenderer", + "core.model.MixinBlockColors", + "core.model.MixinItemColors", + "core.pipeline.MixinBakedQuad", + "core.pipeline.MixinBufferBuilder", + "core.pipeline.MixinVertexConsumer", + "core.pipeline.MixinVertexFormat", + "features.block.MixinBlockModelRenderer", + "features.block.MixinWorldRenderer", + "features.buffer_builder.fast_advance.MixinBufferBuilder", + "features.buffer_builder.fast_advance.MixinVertexFormat", + "features.buffer_builder.fast_sort.MixinBufferBuilder", + "features.buffer_builder.intrinsics.MixinBufferBuilder", + "features.buffer_builder.intrinsics.MixinSpriteTexturedVertexConsumer", + "features.buffer_builder.intrinsics.MixinWorldRenderer", + "features.chunk_rendering.MixinChunkBuilder", + "features.chunk_rendering.MixinClientChunkManager", + "features.chunk_rendering.MixinClientChunkManager$MixinClientChunkMap", + "features.chunk_rendering.MixinClientWorld", + "features.chunk_rendering.MixinPalettedContainer", + "features.chunk_rendering.MixinWorldRenderer", + "features.chunk_rendering.MixinPackedIntegerArray", + "features.chunk_rendering.MixinRenderLayer", + "features.debug.MixinDebugHud", + "features.entity.fast_render.MixinCuboid", + "features.entity.fast_render.MixinModelPart", + "features.entity.smooth_lighting.MixinEntityRenderer", + "features.entity.smooth_lighting.MixinPaintingEntityRenderer", + "features.gui.MixinDebugHud", + "features.gui.fast_loading_screen.MixinLevelLoadingScreen", + "features.gui.fast_status_bars.MixinInGameHud", + "features.gui.fast_fps_pie.MixinMinecraftClient", + "features.gui.font.MixinGlyphRenderer", + "features.item.MixinItemRenderer", + "features.matrix_stack.MixinMatrixStack", + "features.matrix_stack.MixinVertexConsumer", + "features.model.MixinMultipartBakedModel", + "features.model.MixinWeightedBakedModel", + "features.optimized_bamboo.MixinBambooBlock", + "features.options.MixinGameOptions", + "features.options.MixinInGameHud", + "features.options.MixinMinecraftClient", + "features.options.MixinOptionsScreen", + "features.options.MixinWorldRenderer", + "features.particle.fast_render.MixinBillboardParticle", + "features.render_layer.leaves.MixinRenderLayers", + "features.render_layer.leaves.MixinLeavesBlock", + "features.sky.MixinWorldRenderer", + "features.texture_tracking.MixinSprite", + "features.texture_tracking.MixinSpriteAtlasTexture", + "features.texture_tracking.MixinSpriteBillboardParticle", + "features.world_ticking.MixinClientWorld", + "features.fast_biome_colors.MixinBackgroundRenderer", + "features.fast_biome_colors.MixinBlock", + "features.fast_biome_colors.MixinFluid" + ] +} \ No newline at end of file