diff --git a/.editorconfig b/.editorconfig index 6fdb37b49c..c2362ce336 100644 --- a/.editorconfig +++ b/.editorconfig @@ -548,7 +548,7 @@ ij_json_keep_indents_on_empty_lines = false ij_json_keep_line_breaks = true ij_json_space_after_colon = true ij_json_space_after_comma = true -ij_json_space_before_colon = true +ij_json_space_before_colon = false ij_json_space_before_comma = false ij_json_spaces_within_braces = false ij_json_spaces_within_brackets = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..6c889429c3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +/.github/CODEOWNERS @jellysquid3 +/.github/workflows @jellysquid3 diff --git a/.github/workflows/build-tag.yml b/.github/workflows/build-commit-nopublish.yml similarity index 63% rename from .github/workflows/build-tag.yml rename to .github/workflows/build-commit-nopublish.yml index 478c3b3d2b..38f050550e 100644 --- a/.github/workflows/build-tag.yml +++ b/.github/workflows/build-commit-nopublish.yml @@ -1,11 +1,11 @@ -# Used when a commit is tagged and pushed to the repository +# Used when a commit is pushed to the repository # This makes use of caching for faster builds and uploads the resulting artifacts -name: build-tag +name: build-commit-nopublish on: push: - tags: - - '*' + branches-ignore: + - dev jobs: build: @@ -17,23 +17,26 @@ jobs: # bash pattern expansion to grab branch name without slashes run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT id: ref + - name: Checkout sources - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + uses: actions/checkout@v6 + + - name: Setup Java 21 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 with: - cache-read-only: true + cache-read-only: false - name: Execute Gradle build - run: ./gradlew build -Pbuild.release=true + run: ./gradlew build - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sodium-artifacts-${{ steps.ref.outputs.branch }} - path: build/mods/*.jar \ No newline at end of file + path: build/mods/*.jar diff --git a/.github/workflows/build-commit.yml b/.github/workflows/build-commit.yml index b73702a561..cd737b6d7a 100644 --- a/.github/workflows/build-commit.yml +++ b/.github/workflows/build-commit.yml @@ -2,11 +2,15 @@ # This makes use of caching for faster builds and uploads the resulting artifacts name: build-commit -on: [ push ] +on: + push: + branches: + - dev jobs: build: runs-on: ubuntu-latest + environment: staging steps: - name: Extract current branch name @@ -14,20 +18,18 @@ jobs: # bash pattern expansion to grab branch name without slashes run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT id: ref - - name: Checkout sources - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v3 + - name: Checkout sources + uses: actions/checkout@v6 - name: Setup Java 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 with: cache-read-only: false @@ -35,7 +37,14 @@ jobs: run: ./gradlew build - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sodium-artifacts-${{ steps.ref.outputs.branch }} - path: build/mods/*.jar \ No newline at end of file + path: build/mods/*.jar + + - name: Publish Release Snapshot to CaffeineMC Maven + env: + ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }} + ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }} + if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }} + run: ./gradlew publishAllPublicationsToCaffeineMCRepository diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 045dd53920..2ae9dc38fc 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -9,15 +9,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@v3 - - uses: actions/setup-java@v4 + - name: Extract current branch name + shell: bash + # bash pattern expansion to grab branch name without slashes + run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT + id: ref + + - name: Checkout sources + uses: actions/checkout@v6 + + - name: Setup Java 21 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 with: cache-read-only: true @@ -25,7 +33,7 @@ jobs: run: ./gradlew build - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sodium-artifacts-${{ steps.ref.outputs.branch }} - path: build/mods/*.jar \ No newline at end of file + path: build/mods/*.jar diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index ab751d49b9..0000000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Used when a release is pushed to GitHub -# This does not make use of any caching as to ensure a clean build -name: build-release - -on: - release: - types: - - published - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-read-only: true - - - name: Execute Gradle build - run: ./gradlew build -Pbuild.release=true - - - name: Upload assets to GitHub - uses: AButler/upload-release-assets@v2.0 - with: - files: 'build/mods/*.jar' - repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml new file mode 100644 index 0000000000..6580cbb219 --- /dev/null +++ b/.github/workflows/publish-maven.yml @@ -0,0 +1,47 @@ +# Used when a developer decides to publish a release to the caffeinemc maven +# This is primarily for when we manually need to publish a release because for some reason publishing broke +name: Release CaffeineMC Maven + +on: [ workflow_dispatch ] + +jobs: + build: + runs-on: ubuntu-latest + environment: + name: prod + + steps: + - name: Extract current branch name + shell: bash + # bash pattern expansion to grab branch name without slashes + run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT + id: ref + + - name: Checkout sources + uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: true + + - name: Execute Gradle build + run: ./gradlew build -Pbuild.release=true + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: sodium-artifacts-${{ steps.ref.outputs.ref }} + path: build/mods/*.jar + + - name: Publish Tag Release to CaffeineMC Maven + env: + ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }} + ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }} + if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }} + run: ./gradlew publishAllPublicationsToCaffeineMCRepository -Pbuild.release=true diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000000..c16fc4e82d --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,72 @@ +# Used when a developer decides to publish a release +# This makes use of caching for faster builds and uploads the resulting artifacts +name: Release on Platforms +permissions: + contents: write # Needed to publish a GitHub release + +on: + workflow_dispatch: + inputs: + platform: + type: choice + description: Platform + options: + - both + - fabric + - neoforge + destination: + type: choice + description: Destination + options: + - GH+MR+CF + - GH+MR + - GH + +jobs: + build: + runs-on: ubuntu-latest + environment: + name: prod + + steps: + - name: Extract current branch name + shell: bash + # bash pattern expansion to grab branch name without slashes + run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT + id: ref + + - name: Checkout sources + uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: true + + - name: Execute Gradle build + run: ./gradlew build -Pbuild.release=true + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: sodium-artifacts-${{ steps.ref.outputs.branch }} + path: build/mods/*.jar + + - name: Publish Modrinth, Curseforge and Github Releases + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + MODRINTH_API_KEY: ${{ secrets.MODRINTH_API_KEY }} + run: ./gradlew publishMods -Pbuild.release=true -Pbuild.release.platform=${{ inputs.platform }} -Pbuild.release.destination=${{ inputs.destination }} + + - name: Publish Tag Release to CaffeineMC Maven + env: + ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }} + ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }} + if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }} + run: ./gradlew publishAllPublicationsToCaffeineMCRepository -Pbuild.release=true diff --git a/.gitignore b/.gitignore index 69cd50978c..ec97f0641b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ neoforge/runs .DS_Store .AppleDouble .LSOverride + +# Kotlin compiler working directory +.kotlin \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..8db93dd5e8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +[ReleaseTag]() is automatically replaced with the release tag, e.g. mc26.1-0.8.9 +[MCVersion]() is automatically replaced with the minecraft version, e.g. 26.1 +[SodiumVersion]() is automatically replaced with the sodium version, e.g. 0.8.9 +Everything above the line is ignored and not included in the changelog. Everything below will be in the +changelog on GitHub, Modrinth and CurseForge. +---------- +### Overview +Sodium [SodiumVersion]() is a backport of modern Sodium 0.8 to Minecraft [MCVersion](). + +- Significantly improved the performance of rendering the world (up to +115%) on some computers. +- Greatly improved the rendering of transparent objects with complex models, especially when submerged in water. +- Lots and lots of improvements to the user experience in the Video Settings menu. +- Reduced latency and micro-stutter when updating chunks in the world. +- Slightly faster entity rendering, especially for transparent mobs and particles. +- Improvements for hardware and mod compatibility. +- ...And many more bug fixes and improvements... + +### Using and Building on This Release +It includes the backport of our Config API and other conventions that will hopefully make it easier for mods to interact with Sodium across multiple versions. This release series doesn't get released at the same cadence as our current releases for Minecraft 26.2 and 26.1, and doesn't follow the same alpha/beta numbering. Mod developers can find our artifacts, such as the Config API, on [our Maven repository](https://maven.caffeinemc.net/). + +Mod compatibility: +- Create Aeronautics works as of 1.3.0 +- Sable works as of 2.0.0 +- Veil works as of 4.1.2 +- Iris works as of 1.8.13 +- Not compatible with any version of "Sodium Options API", which is not affiliated with us, and any mods that depend on it. + +Some other mods may still be incompatible with this release. If your modpack doesn't work with this release, downgrade to the version of Sodium it was built with and choose appropriate versions of other mods. New or updated modpacks are encouraged to use the latest version of Sodium and compatible mods. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60221b425c..9ca9757920 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,10 @@ When submitting a pull request, you are granting [JellySquid](https://jellysquid.me) the right to license your contributions under the [Polyform Shield License (Version 1.0.0)](LICENSE.md). +Without exception, it is required that all code being submitted to the project is of your original work. We +will not accept pull requests which contain *any amount* of AI-generated code, regardless of the particular +circumstances. + If you have any questions about these terms, please [get in contact with us](https://caffeinemc.net/discord). ### Code Style @@ -56,4 +60,4 @@ style guidelines. If you're adding new Mixin patches to the project, please ensure that you have created appropriate entries to disable them in the config file. Mixins should always be self-contained and grouped into "patch sets" which are easy to isolate, -and where that is not possible, they should be placed into the "core" package. \ No newline at end of file +and where that is not possible, they should be placed into the "core" package. diff --git a/README.md b/README.md index e80d00e483..ab9929ea6a 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,49 @@ Sodium is a powerful rendering engine and optimization mod for the Minecraft client which improves frame rates and reduces micro-stutter, while fixing many graphical issues in Minecraft. -### 📥 Installation +**This mod is the result of thousands of hours of development, and is made possible thanks to players like you.** If you +would like to show a token of your appreciation for my work, and help support the development of Sodium in the process, +then consider [buying me a coffee](https://caffeinemc.net/donate). -The latest version of Sodium can be downloaded from our official [Modrinth](https://modrinth.com/mod/sodium) and -[CurseForge](https://www.curseforge.com/minecraft/mc-mods/sodium) pages. + + +--- + +### 📥 Downloads + +#### Stable builds + +The latest stable release of Sodium can be downloaded from our official [Modrinth](https://modrinth.com/mod/sodium) and +[CurseForge](https://www.curseforge.com/minecraft/mc-mods/sodium) pages. + +#### Nightly builds (for developers) + +We also provide bleeding-edge builds ("nightlies") which are useful for testing the very latest changes before they're +packaged into a release. These builds are primarily intended for other mod developers and users with expert skills, and do +not come with any support or warranty. + +For a complete listing of available nightly builds, please see [the wiki page](https://github.com/CaffeineMC/sodium/wiki/Nightly-Builds). We also have a Maven repository for including Sodium in your development workspace or build process, for which you can also find [documentation on our wiki](https://github.com/CaffeineMC/sodium/wiki/CaffeineMC-Maven-&-Config-API). + +### 🖥️ Installation Since the release of Sodium 0.6.0, both the _Fabric_ and _NeoForge_ mod loaders are supported. We generally recommend that new users prefer to use the _Fabric_ mod loader, since it is more lightweight and stable (for the time being.) For more information about downloading and installing the mod, please refer to our [Installation Guide](https://github.com/CaffeineMC/sodium/wiki/Installation). -### 🐛 Reporting Issues +### 🙇 Getting Help + +For technical support (including help with mod installation problems and game crashes), please use our +[official Discord server](https://caffeinemc.net/discord). -You can report bugs and crashes by opening an issue on our [issue tracker](https://github.com/CaffeineMC/sodium/issues). -Before opening a new issue, use the search tool to make sure that your issue has not already been reported and ensure -that you have completely filled out the issue template. Issues that are duplicates or do not contain the necessary -information to triage and debug may be closed. +### 📬 Reporting Issues + +If you do not need technical support and would like to report an issue (bug, crash, etc.) or otherwise request changes +(for mod compatibility, new features, etc.), then we encourage you to open an issue on the +[project issue tracker](https://github.com/CaffeineMC/sodium/issues). Please note that while the issue tracker is open to feature requests, development is primarily focused on -improving hardware compatibility, performance, and finishing any unimplemented features necessary for parity with +improving compatibility, performance, and finishing any unimplemented features necessary for parity with the vanilla renderer. ### 💬 Join the Community @@ -36,45 +60,40 @@ We have an [official Discord community](https://caffeinemc.net/discord) for all ## ✅ Hardware Compatibility -We only provide support for graphics cards which have up-to-date drivers for OpenGL 4.6. Most graphics cards which have -been released since year 2010 are supported, such as the... +We only provide official support for graphics cards which have up-to-date drivers that are compatible with OpenGL 4.5 +or newer. Most graphics cards released in the past 12 years will meet these requirements, including the following: - AMD Radeon HD 7000 Series (GCN 1) or newer - NVIDIA GeForce 400 Series (Fermi) or newer - Intel HD Graphics 500 Series (Skylake) or newer -In some cases, older graphics cards may also work (so long as they have up-to-date drivers which have support for -OpenGL 3.3), but they are not officially supported, and may not be compatible with future versions of Sodium. +Nearly all graphics cards that are already compatible with Minecraft (which requires OpenGL 3.3) should also work +with Sodium. But our team cannot ensure compatibility or provide support for older graphics cards, and they may +not work with future versions of Sodium. #### OpenGL Compatibility Layers -Devices which need to use OpenGL translation layers (such as GL4ES, ANGLE, etc) are not supported and will very likely -not work with Sodium. These translation layers do not implement required functionality and they suffer from underlying +Devices which need to use OpenGL translation layers (such as GL4ES, ANGLE, etc.) are not supported and will very likely +not work with Sodium. These translation layers do not implement required functionality, and they suffer from underlying driver bugs which cannot be worked around. -## 🛠️ Developer Guide - -### Building from sources +## 🛠️ Building from sources -Sodium uses a typical Gradle project structure and can be compiled by simply running the default `build` task. The build -artifacts (typical mod binaries, and their sources) can be found in the `build/libs` directory. +Sodium uses the [Gradle build tool](https://gradle.org/) and can be built with the `gradle build` command. The build +artifacts (production binaries and their source bundles) can be found in the `build/mods` directory. -#### Requirements +The [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:using_wrapper) is provided for ease of use and will automatically download and install the +appropriate version of Gradle for the project build. To use the Gradle wrapper, substitute `gradle` in build commands +with `./gradlew.bat` (Windows) or `./gradlew` (macOS and Linux). -We recommend using a package manager (such as [SDKMAN](https://sdkman.io/)) to manage toolchain dependencies and keep -them up to date. For many Linux distributions, these dependencies will be standard packages in your software -repositories. +### Build Requirements - OpenJDK 21 - - We recommend using the [Eclipse Temurin](https://adoptium.net/) distribution, as it's known to be high quality - and to work without issues. -- Gradle 8.6.x (optional) - - The [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:using_wrapper) is provided - in this repository can be used instead of installing a suitable version of Gradle yourself. However, if you are - building many projects, you may prefer to install it yourself through a suitable package manager as to save disk - space and to avoid many different Gradle daemons sitting around in memory. + - We recommend using the [Eclipse Temurin](https://adoptium.net/) distribution as it's regularly tested by our developers and known + to be of high quality. +- Gradle 8.10.x - Typically, newer versions of Gradle will work without issues, but the build script is only tested against the - version specified by the wrapper script. + version used by the [wrapper script](/gradle/wrapper/gradle-wrapper.properties). ## 📜 License diff --git a/build.gradle.kts b/build.gradle.kts index e69de29bb2..7f89a9f9be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -0,0 +1,97 @@ +import me.modmuss50.mpp.ReleaseType +import me.modmuss50.mpp.platforms.curseforge.CurseforgeOptions +import me.modmuss50.mpp.platforms.modrinth.ModrinthOptions +import java.util.* + +plugins { + id("me.modmuss50.mod-publish-plugin") version("1.1.0") +} + +gradle.projectsEvaluated { + publishMods { + if (!project.hasProperty("build.release")) { + return@publishMods println("Publishing is disabled, please use the CI publishing workflow") + } + + val releasePlatform: String = project.providers.gradleProperty("build.release.platform").orNull + ?: return@publishMods println("build.release.platform must be defined (expected: both, fabric, neoforge)") + + val releaseDestination: String = project.providers.gradleProperty("build.release.destination").orNull + ?: return@publishMods println("build.release.destination must be defined (expected: GH+MR+CF, GH+MR, GH)") + val publishModrinth = releaseDestination.contains("MR") + val publishCurseforge = releaseDestination.contains("CF") + + val modVersion = BuildConfig.createVersionString(project); + + type = when { + modVersion.contains("alpha") -> ReleaseType.ALPHA + modVersion.contains("beta") -> ReleaseType.BETA + else -> ReleaseType.STABLE + } + changelog = BuildConfig.getChangelog(project) + + val curseforgeShared = curseforgeOptions { + accessToken = project.providers.environmentVariable("CURSEFORGE_API_KEY") + projectId = BuildConfig.CURSEFORGE_PROJECT_ID + minecraftVersions.add(BuildConfig.MINECRAFT_VERSION) + } + + val modrinthShared = modrinthOptions { + accessToken = project.providers.environmentVariable("MODRINTH_API_KEY") + projectId = BuildConfig.MODRINTH_PROJECT_ID + minecraftVersions.add(BuildConfig.MINECRAFT_VERSION) + } + + setupFor("Fabric", releasePlatform, publishCurseforge, publishModrinth, curseforgeShared, modrinthShared) + setupFor("NeoForge", releasePlatform, publishCurseforge, publishModrinth, curseforgeShared, modrinthShared) + + github { + accessToken = project.providers.environmentVariable("GITHUB_TOKEN") + repository = "CaffeineMC/sodium" + commitish = BuildConfig.calculateGitHash(project) + version = BuildConfig.RELEASE_TAG + displayName = "Sodium ${BuildConfig.MOD_VERSION} for Minecraft ${BuildConfig.MINECRAFT_VERSION}" + file.unset() + file.unsetConvention() + + allowEmptyFiles = true + } + } +} + +fun me.modmuss50.mpp.ModPublishExtension.setupFor(loaderName: String, releasePlatform: String, publishCurseforge: Boolean, publishModrinth: Boolean, curseforgeOptions: Provider, modrinthOptions: Provider) { + val loaderLowercase = loaderName.lowercase(Locale.ROOT) + + if (releasePlatform == "both" || releasePlatform == loaderLowercase) { + val taskName = if (loaderLowercase == "fabric") "remapJar" else "jar" + val jar = project(":$loaderLowercase").tasks.getByName(taskName).outputs.files.singleFile + + val releaseTitle = "Sodium ${BuildConfig.MOD_VERSION} for $loaderName ${BuildConfig.MINECRAFT_VERSION}" + val releaseVersion = "${BuildConfig.RELEASE_TAG}-$loaderLowercase" + + if (publishCurseforge) { + curseforge("curseforge$loaderName") { + from(curseforgeOptions) + + file.set(jar) + displayName = releaseTitle + version = releaseVersion + modLoaders.add(loaderLowercase) + + clientRequired = true + serverRequired = false + } + } + + if (publishModrinth) { + modrinth("modrinth$loaderName") { + from(modrinthOptions) + + file.set(jar) + displayName = releaseTitle + version = releaseVersion + modLoaders.add(loaderLowercase) + } + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index 9d9844cf3b..1a94f09ba8 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -2,16 +2,25 @@ import org.gradle.api.Project object BuildConfig { val MINECRAFT_VERSION: String = "1.21.1" - val NEOFORGE_VERSION: String = "21.1.46" - val FABRIC_LOADER_VERSION: String = "0.16.4" - val FABRIC_API_VERSION: String = "0.103.0+1.21.1" + val NEOFORGE_VERSION: String = "21.1.228" + val FABRIC_LOADER_VERSION: String = "0.19.2" + val FABRIC_API_VERSION: String = "0.116.11+1.21.1" // This value can be set to null to disable Parchment. - // TODO: Re-add Parchment - val PARCHMENT_VERSION: String? = null + val PARCHMENT_VERSION: String? = "2024.11.17" // https://semver.org/ - var MOD_VERSION: String = "0.6.0-beta.2" + val MOD_VERSION: String = "0.8.12" + + val MINECRAFT_VERSION_SHORT: String = MINECRAFT_VERSION + .replace("-snapshot-", "s") + .replace("-pre-", "p") + .replace("-rc-", "r") + + val RELEASE_TAG: String = "mc$MINECRAFT_VERSION_SHORT-$MOD_VERSION" + + val CURSEFORGE_PROJECT_ID = "394468" + val MODRINTH_PROJECT_ID = "AANobbMI" fun createVersionString(project: Project): String { val builder = StringBuilder() @@ -23,10 +32,10 @@ object BuildConfig { builder.append(MOD_VERSION) } else { builder.append(MOD_VERSION.substringBefore('-')) - builder.append("-snapshot") + builder.append("-SNAPSHOT") } - builder.append("+mc").append(MINECRAFT_VERSION) + builder.append("+mc").append(MINECRAFT_VERSION_SHORT) if (!isReleaseBuild) { if (buildId != null) { @@ -38,4 +47,21 @@ object BuildConfig { return builder.toString() } -} \ No newline at end of file + + fun calculateGitHash(project: Project): String = try { + val output = project.providers.exec { + workingDir(project.projectDir) + commandLine("git", "rev-parse", "HEAD") + } + output.standardOutput.asText.get().trim() + } catch (_: Throwable) { + "unknown" + } + + fun getChangelog(project: Project): String = project.rootProject.file("CHANGELOG.md").readText() + .split("----------")[1] + .trim() + .replace("[ReleaseTag]()", RELEASE_TAG) + .replace("[MCVersion]()", MINECRAFT_VERSION) + .replace("[SodiumVersion]()", MOD_VERSION) +} diff --git a/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts b/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts index 006f9f3500..55b89b8cba 100644 --- a/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts +++ b/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts @@ -16,7 +16,7 @@ tasks { inputs.property("version", version) filesMatching(listOf("fabric.mod.json", "META-INF/neoforge.mods.toml")) { - expand(mapOf("version" to version)) + expand(mapOf("version" to inputs.properties["version"])) } } @@ -31,13 +31,23 @@ tasks { } publishing { - publications { - create("maven") { - groupId = project.group as String - artifactId = project.name as String - version = version - - from(components["java"]) + // Each platform is responsible for their own "publications". + + repositories { + val isReleaseBuild = project.hasProperty("build.release") + val caffeineMCMavenUsername: String? by project // reads from ORG_GRADLE_PROJECT_caffeineMCMavenUsername + val caffeineMCMavenPassword: String? by project // reads from ORG_GRADLE_PROJECT_caffeineMCMavenPassword + + maven { + name = "CaffeineMC" + url = uri("https://maven.caffeinemc.net".let { + if (isReleaseBuild) "$it/releases" else "$it/snapshots" + }) + + credentials { + username = caffeineMCMavenUsername + password = caffeineMCMavenPassword + } } } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index cec4b9fdeb..d9532ec6e6 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("multiloader-base") id("java-library") - id("fabric-loom") version ("1.8.9") + id("net.fabricmc.fabric-loom-remap") version ("1.16.1") } base { @@ -16,7 +16,7 @@ val configurationPreLaunch = configurations.create("preLaunchDeps") { sourceSets { val main = getByName("main") val api = create("api") - val workarounds = create("workarounds") + val boot = create("boot") api.apply { java { @@ -24,7 +24,7 @@ sourceSets { } } - workarounds.apply { + boot.apply { java { compileClasspath += configurationPreLaunch } @@ -33,15 +33,19 @@ sourceSets { main.apply { java { compileClasspath += api.output - compileClasspath += workarounds.output + compileClasspath += boot.output } } create("desktop") } +repositories { + mavenLocal() +} + dependencies { - minecraft(group = "com.mojang", name = "minecraft", version = BuildConfig.MINECRAFT_VERSION) + minecraft("com.mojang:minecraft:${BuildConfig.MINECRAFT_VERSION}") mappings(loom.layered { officialMojangMappings() @@ -50,8 +54,8 @@ dependencies { } }) - compileOnly("io.github.llamalad7:mixinextras-common:0.3.5") - annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5") + compileOnly("io.github.llamalad7:mixinextras-common:0.5.3") + annotationProcessor("io.github.llamalad7:mixinextras-common:0.5.3") compileOnly("net.fabricmc:sponge-mixin:0.13.2+mixin.0.8.5") compileOnly("net.fabricmc:fabric-loader:${BuildConfig.FABRIC_LOADER_VERSION}") @@ -69,13 +73,12 @@ dependencies { // We need to be careful during pre-launch that we don't touch any Minecraft classes, since other mods // will not yet have an opportunity to apply transformations. - configurationPreLaunch("org.apache.commons:commons-lang3:3.14.0") - configurationPreLaunch("commons-io:commons-io:2.15.1") configurationPreLaunch("org.lwjgl:lwjgl:3.3.3") + configurationPreLaunch("org.lwjgl:lwjgl-opengl:3.3.3") configurationPreLaunch("net.java.dev.jna:jna:5.14.0") configurationPreLaunch("net.java.dev.jna:jna-platform:5.14.0") configurationPreLaunch("org.slf4j:slf4j-api:2.0.9") - configurationPreLaunch("org.jetbrains:annotations:25.0.0") + configurationPreLaunch("org.jetbrains:annotations:24.1.0") } loom { @@ -98,6 +101,21 @@ fun exportSourceSetJava(name: String, sourceSet: SourceSet) { } } +fun exportSourceSetSources(name: String, sourceSet: SourceSet) { + val configuration = configurations.create("${name}Sources") { + isCanBeResolved = true + isCanBeConsumed = true + } + + val compileTask = tasks.register(sourceSet.getTaskName("process", "sources")) { + from(sourceSet.allSource) + into(file(project.layout.buildDirectory).resolve("sources").resolve(sourceSet.name)) + }.get() + artifacts.add(configuration.name, compileTask.destinationDir) { + builtBy(compileTask) + } +} + fun exportSourceSetResources(name: String, sourceSet: SourceSet) { val configuration = configurations.create("${name}Resources") { isCanBeResolved = true @@ -118,13 +136,14 @@ fun exportSourceSetResources(name: String, sourceSet: SourceSet) { // Exports the compiled output of the source set to the named configuration. fun exportSourceSet(name: String, sourceSet: SourceSet) { exportSourceSetJava(name, sourceSet) + exportSourceSetSources(name, sourceSet) exportSourceSetResources(name, sourceSet) } exportSourceSet("commonMain", sourceSets["main"]) exportSourceSet("commonApi", sourceSets["api"]) -exportSourceSet("commonEarlyLaunch", sourceSets["workarounds"]) +exportSourceSet("commonBoot", sourceSets["boot"]) exportSourceSet("commonDesktop", sourceSets["desktop"]) tasks.jar { enabled = false } -tasks.remapJar { enabled = false } \ No newline at end of file +tasks.remapJar { enabled = false } diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java index b5de855a17..5a1e7054af 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java @@ -1,7 +1,5 @@ package net.caffeinemc.mods.sodium.api.blockentity; -import java.util.function.Predicate; - import net.caffeinemc.mods.sodium.api.internal.DependencyInjection; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java new file mode 100644 index 0000000000..ecb89e228f --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java @@ -0,0 +1,29 @@ +package net.caffeinemc.mods.sodium.api.config; + +import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder; + +/** + * Entry point interface for registering configuration structure. Typically, mods will only need the "late" registration method unless they need to do something very early in the mod loading process and need their options to be resolved before that point. + *

+ * The {@link ConfigBuilder} instance passed to these methods already knows which mod is registering the configuration, so there is typically no need to specify the mod metadata manually. + */ +public interface ConfigEntryPoint { + /** + * Register configuration structure early in the mod loading process. + * This method is called before most mods have had a chance to initialize. + * Use this method only if you need your configuration options to be available very early. + * + * @param builder The configuration builder to register options with. + */ + default void registerConfigEarly(ConfigBuilder builder) { + } + + /** + * Register configuration structure later in the mod loading process. + * This method is called after most mods have initialized. + * This is the preferred method for registering configuration options. + * + * @param builder The configuration builder to register options with. + */ + void registerConfigLate(ConfigBuilder builder); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java new file mode 100644 index 0000000000..b56265a19a --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.api.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as a configuration entry point for Sodium on the NeoForge platform. + * This annotation should be placed on classes that implement the configuration + * entry point interface to associate them with a specific mod id. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigEntryPointForge { + /** + * The mod id to associate this config entrypoint's "owner" with. + * + * @return the mod id + */ + String value(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java new file mode 100644 index 0000000000..f823841a46 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java @@ -0,0 +1,44 @@ +package net.caffeinemc.mods.sodium.api.config; + +import net.minecraft.resources.ResourceLocation; + +/** + * Represents the current state of configuration options. This interface is accessed through dynamic value providers throughout the API. Only declared dependencies of a dynamic value provider are allowed to be queried (and doing otherwise will result in a crash). + */ +public interface ConfigState { + /** + * Special option ID to be passed as a dependency of a dynamic value provider to indicate that the provider should be re-evaluated when the configuration screen is rebuilt. It is unlikely that you need to use this. + */ + ResourceLocation UPDATE_ON_REBUILD = ResourceLocation.parse("__meta__:update_on_rebuild"); + + /** + * Special option ID to be passed as a dependency of a dynamic value provider to indicate that the provider should be re-evaluated when the value for the parent option is applied. This allows the dynamic value to read the value of the option itself, which would be an error otherwise. It does not allow reading other options unless they are also declared as dependencies. + */ + ResourceLocation UPDATE_ON_APPLY = ResourceLocation.parse("__meta__:update_on_apply"); + + /** + * Reads a boolean option from the configuration state. + * + * @param id The ID of the option. + * @return The current value of the boolean option. + */ + boolean readBooleanOption(ResourceLocation id); + + /** + * Reads an integer option from the configuration state. + * + * @param id The ID of the option. + * @return The current value of the integer option. + */ + int readIntOption(ResourceLocation id); + + /** + * Reads an enum option from the configuration state. + * + * @param id The ID of the option. + * @param enumClass The class of the enum. + * @param The enum type. + * @return The current value of the enum option. + */ + > E readEnumOption(ResourceLocation id, Class enumClass); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java new file mode 100644 index 0000000000..9555ec63e7 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.api.config; + +/** + * Functional interface for handling storage events, such as after a configuration has been saved. + */ +@FunctionalInterface +public interface StorageEventHandler { + /** + * Called after options have been saved to their bindings. This is typically used for flushing changes to disk. + */ + void afterSave(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md new file mode 100644 index 0000000000..42e71fc7b1 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md @@ -0,0 +1,272 @@ +# Usage of the Sodium Config API + +The Sodium Config API lets mods add their own pages to the Video Settings screen, which Sodium replaces with its own screen. + +If you encounter difficulties using the API, find bugs in it, or it's missing features you need, don't hesitate to contact us or make a contribution directly. + +## Scope + +The Sodium Config API is intended for mods that add video settings, not as a general purpose config API. For general purpose configuration, use the platform's appropriate mod list and a config library. + +As a presentation API, it does not handle loading, parsing, or saving configuration data to files. It is up to your mod to handle that on its own, or use a third-party library dedicated to that task. + +## Overview + +Sodium redirects Minecraft's "Video Settings" screen to its own screen. Historically, third-party mods have mixed into Sodium to add buttons to their own settings pages or additional options to Sodium's pages. + +With this API, these mods will not need to touch Sodium's internals anymore and should be able to operate independently of the GUI's implementation details. The API may not be able to cover all use cases where mods mixed into Sodium's options code, but it should cover most of the common ones. + +Registration of options happens in two stages: Early and late. Early registration happens when Sodium initializes its own early options before the window is created. Late registration happens after the game launched. Most mods will only need to use late registration. These stages are independent and only options that are registered in the late stage will show up in the GUI. + +### Features of the API + +Here's a summary of the features this config API provides: + +- a list of option pages per mod + - ID, name, and version + - a theme color + - optionally a square monochrome icon +- a list of option groups per page + - optionally named option groups + - external pages that simply open a mod-defined `Screen` +- a list of options per group +- options of various types + - `ResourceIdentifiers` as IDs + - types: integer slider, enum, boolean, external (opens a new `Screen`) + - value bindings through callbacks + - dynamic values for enablement, default value, allowed values + - dynamic values may depend on other option's values + - cycle and declared dependency consistency checking + - value formatters for presentation + - value-dependent tooltips + - flags for renderer reload and other graphics events +- all user facing strings are translatable +- storage providers are flushed after changes are applied +- early registration for mods that need to have their options available before the window is created +- entrypoint-based config registration +- declarative builder-based style +- option overlay or replacement to modify existing options + - overlaying only changes some properties of another option +- custom flags to perform actions after certain changes + +## Getting Started + +### Dependency on Sodium's API + +Sodium publishes its api package on a maven repository that you can depend on in your buildscript. It needs `modImplementation` on Fabric 1.21.11, and `implementation` on Fabric 1.21.12+ and NeoForge 1.21.11+. + +Fabric: + +```groovy +dependencies { + // ... other dependencies + + // using a tagged API build + modImplementation "net.caffeinemc:sodium-fabric-api:0.8.0+mc1.21.11" + // OR using a snapshot build + modImplementation "net.caffeinemc:sodium-fabric-api:0.8.0-SNAPSHOT+mc1.21.11-pre3-build.773" +} +``` + +NeoForge: + +```groovy +dependencies { + // ... other dependencies + + // using a tagged API build + implementation "net.caffeinemc:sodium-neoforge-api:0.8.0+mc1.21.11" + // OR using a snapshot build + implementation "net.caffeinemc:sodium-neoforge-api:0.8.0-SNAPSHOT+mc1.21.11-pre3-build.773" +} +``` + +Make sure you have our Maven repository declared: + +```groovy +maven { + name "CaffeineMC" + url "https://maven.caffeinemc.net/releases" // or /snapshots +} +``` +Kotlin: +```kotlin +maven { + name = "CaffeineMC" + url = uri("https://maven.caffeinemc.net/releases") // or /snapshots +} +``` + +The Maven Repository has a web frontend that you can use to view the list of available versions: https://maven.caffeinemc.net + +### Creating an Entrypoint + +Entrypoint classes that Sodium calls to run your options registration code can be declared either in your mod's metadata file, or on NeoForge with a special annotation. + +#### With a Metadata Entry + +Metadata-based entrypoints use the key `sodium:config_api_user` and the value is the full reference to a class that implements the `net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint` interface. + +Fabric `fabric.mod.json`: + +```json5 +{ + "entrypoints": { + // ... other entrypoints + + "sodium:config_api_user": [ + "com.example.examplemod.ExampleModConfigBuilder" + ] + } +} +``` + +NeoForge `neoforge.mods.toml`: +```toml +[modproperties.examplemod] +"sodium:config_api_user" = "com.example.examplemod.ExampleModConfigBuilder" +``` + +The implementation of the entrypoint can look something like this: + +```java +package com.example.examplemod; + +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint; +import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +public class ExampleConfigUser implements ConfigEntryPoint { + private final OptionStorage storage = new OptionStorage(); + private final Runnable handler = this.storage::flush; // typically gets referenced many times + + @Override + public void registerConfigLate(ConfigBuilder builder) { + builder.registerOwnModOptions() + .setIcon(ResourceLocation.parse("examplemod:textures/gui/icon.png")) + .addPage(builder.createOptionPage() + .setName(Component.literal("Example Page")) + .addOptionGroup(builder.createOptionGroup() + .setName(Component.literal("Example Group")) // only if necessary for clarity + .addOption(builder.createBooleanOption(ResourceLocation.parse("examplemod:example_option")) + .setName(Component.literal("Example Option")) // use translation keys here + .setTooltip(Component.literal("Example tooltip")) + .setStorageHandler(this.handler) + .setBinding(this.storage::setExampleOption, this.storage::getExampleOption) + .setDefaultValue(true) + ) + ) + ); + } +} + +// OptionStorage.java +class OptionStorage { + private boolean exampleOption = true; + + public boolean getExampleOption() { + return this.exampleOption; + } + + public void setExampleOption(boolean value) { + this.exampleOption = value; + } + + public void flush() { + // flush options to config file + } +} +``` + +#### NeoForge: With an Annotation + +Since NeoForge has the convention of using annotations for entrypoints, this option is provided as an alternative. Any classes annotated with `@ConfigEntryPointForge("examplemod")` will be loaded as config entrypoints too. Note that the annotation must be given the mod id that should be associated as the default mod for which a config is registered with `ConfigBuilder.registerOwnModOptions`. This is necessary as it's otherwise impossible to uniquely determine which mod a class is associated with on NeoForge. + +```java +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint; +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge; + +@ConfigEntryPointForge("examplemod") +public class ExampleConfigUser implements ConfigEntryPoint { + // class body identical to the above +} +``` + +### Registering Your Options + +Each mod adds a page for its options, within each page there are groups of options, and each group contains a list of options. Each option has an id, a name, a tooltip, a storage handler, a binding, and a default value. There are three types of options: boolean (tickbox), integer (slider), and enum. Optionally, all types of options can be disabled, while integer and enum options can have their allowed values restricted. Those two types also require you to set a function that assigns a label to each selected value. + +Some attributes of an option can be provided dynamically, meaning the returned value can depend on the state of another option. The default value, option enablement, and the allowed values can be computed dynamically. The methods for setting a dynamic value provider also require you to specify a list of dependencies, which are the resource locations of the options that the dynamic value provider reads from. Since dynamically evaluated attributes may change the state of an option, cyclic dependencies will lead to option registration failing and the game crashing. + +Sodium constructs one instance of the entrypoint class, and then calls the early and late registration methods at the right time. + +### Icon Design + +The UI is designed with monochrome, binary-alpha icons in mind. These icons are tinted the color of each mod's theme. We recommend that API users avoid displaying an icon that isn't appropriately styled. However, users can choose to deviate from this convention by presenting a regular, full-color icon using the means provided in the API. + +It is not necessary to explicitly set a theme color, as the system will automatically select one from a predefined set. However, if you set a custom theme color, it should have a minimum level of saturation to remain true to its purpose as an accent color. For aesthetic and accessibility reasons, it is also not reasonable to set the brightness/value of the theme color to be too dark. The theme base color should also not be too bright, otherwise the highlight color, which is brighter, will have no contrast against it. + +## API Notes + +The API is largely self-explanatory and an example is provided above. Also see Sodium's own options registration for a more in-depth example of the API's usage. + +### Using `ConfigBuilder` and `ModOptions` + +The `ConfigBuilder` instance passed to the registration method allows quick and easy registration of a mod's own options using `ConfigBuilder.registerOwnModOptions`. The mod's id, name, version or a formatter for the existing version, and the color theme can be configured on the returned `ModOptionsBuilder`. It's also possible to register options for additional mods using `ConfigBuilder.registerModOptions`. Which mod is the "own" mod for `registerOwnModOptions` is determined by the mod that owns the metadata-based entrypoint or the mod id passed to the `@ConfigEntryPointForge("examplemod")` annotation. + +Each registered mod gets its own header in the page list. The color of the header and the corresponding entries is randomly selected from a predefined list by default, but can be customized using `ModOptionsBuilder.setColorTheme`. A color theme is created either by specifying three (A)RGB colors or a single base color with the lighter and darker colors getting derived automatically. A mod can also specify an icon with `ModOptionsBuilder.setIcon`, which takes a `ResourceLocation` pointing to a texture, which will be tinted in the theme color and rendered in its entirety as a square. Icons set with `ModOptionsBuilder.setNonTintedIcon` are not tinted. + +To simply switch to a new `Screen` when an entry in the video settings screen's page list is clicked, use `ConfigBuilder.createExternalPage` and add the returned page normally after configuring it with a name and a `Consumer` that receives the current screen and switches to your custom screen. + +### Using `OptionBuilder` + +The storage handler set with `OptionBuilder.setStorageHandler` is called after changes have been made to the options through the bindings. This lets you flush the changes to the config file once, instead of every time an option is changed. + +The tooltip set with `OptionBuilder.setTooltip` can optionally be a function that generates a tooltip depending on the option's current value. This is useful for enum options for which the description would be too long otherwise. + +Optionally a performance impact can be specified with `OptionBuilder.setImpact` where the impact ranges from low to high (or "varies"). + +Flags set with `OptionBuilder.setFlags` control what things are reset when this option is applied. They include reloading chunks or reloading resource packs. See `OptionFlag` for the available values. + +The default value set with `OptionBuilder.setDefaultValue`, or dynamically with `OptionBuilder.setDefaultProvider`, is used if the value returned by the binding does not fulfill the option's value constraint (in the case of an integer or enum option). + +Disabling an option with `OptionBuilder.setEnabled(false)` shows the option as strikethrough and makes it non-interactive. Otherwise, especially with regard to value constraints, it will behave as usual. + +The binding configured with `OptionBuilder.setBinding` is called when changes to the options have been made and are applied, or when the value no longer fulfills the option's constraints and is reset to the default value. It's also used to initially load a value during initialization. + +The hook given with `OptionBuilder.setApplyHook` is run after the option has been saved if its valued changed. The hook is given access to the current `ConfigState`, without needing to declare dependencies since the hook does not return anything. This is equivalent to making a singleton custom flag for this option and registering a flag hook for it, and it is implemented this way internally. + +### Using `? extends OptionBuilder` + +Some of the attributes of an option are required and you must set them, or registration will fail. The concrete extensions of `OptionBuilder` for each of the option types have some additional methods for configuring type-specific things, some of which are also required. Notably, `EnumOptionBuilder.setElementNameProvider` and `IntegerOptionBuilder.setValueFormatter` are required in order to display these types of options. The method `setValueFormatter` for integer options takes a `ControlValueFormatter`, which simply formats a number as a `Component`. Many standard value formatters are provided in `ControlValueFormatterImpls` (not part of the API package). + +Integer and enum options can have value constraints that restrict the set of allowed values the user can select. For integer options, a `Range` must be configured with `IntegerOptionBuilder.setRange` so that the slider's start, end, and step positions are well-defined. Enum options may be configured to only allow the selection of certain elements with `EnumOptionBuilder.setAllowedValues`. + +### Using Option Overlays and Replacement + +It's a typical use case to want to change one of Sodium's or another mod's options to have a different range or behavior. This can be achieved in one of two ways: by replacing the option or by applying an overlay to it. + +#### Replacement + +You can replace another option with `ModOptionsBuilder.registerOptionReplacement` by supplying a target identifier and a new option built as usual. The ID of the new option replaces the ID of the old option, and you can optionally choose to adopt the old option's ID to remain a target for overlays targeting it. + +#### Overlays + +Changing only some aspects of an option is possible through `ModOptionsBuilder.registerOptionOverlay`. You supply a partial option by using the same option builder as one usually does. The properties of the target options are replaced with the new ones if specified. + +### Custom Flags + +By registering a flag hook with `ModOptionsBuilder.registerFlagHook` you can run code after a flag from the specified set is triggered. After an option is applied and its value has been saved, all of its attached flags are triggered. The st of trigger flags for `registerFlagHook` can include custom flags, which are simply an arbitrary `Identifier`, or one of the meta flags, such as `OptionFlag.REQUIRES_RENDERER_RELOAD.getId()`. + +### Dependent Values + +To use a dynamic value method of the form `set___Provider`, you need to pass it a value provider. This provider is called with a reference to the current state of the options store. In order to be allowed to read the values of other options from it, you need to pass the IDs of the options this provider depends on as arguments to the method. Reading from options that are not declared as a dependency throws an exception. The system makes sure no dynamic value of an option depends on its own value or other options' values that indirectly depend on the first option, i.e. cycles are forbidden. + +#### Special Dependencies + +Providers that declare a dependency on `ConfigState.UPDATE_ON_REBUILD` are refreshed when the UI is rebuilt. + +Providers that declare a dependency on `ConfigState.UPDATE_ON_APPLY` are refreshed when the modified value of the option they are attached to is applied. This is the only situation in which a dynamic valued property of an option is allowed to depend on its containing options' value. + +Both of these special dependencies are typically not needed and making use of them is most likely user error. Sodium only uses them for making sure the extremely weird GUI Scale option behaves as expected. \ No newline at end of file diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java new file mode 100644 index 0000000000..e6df0bc8c5 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import net.minecraft.network.chat.Component; + +/** + * A formatter for control values, converting integer values into display components. + */ +@FunctionalInterface +public interface ControlValueFormatter { + /** + * Formats the given integer value into a display component. + * + * @param value the integer value to format + * @return the formatted value + */ + Component format(int value); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java new file mode 100644 index 0000000000..d7784db134 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.minecraft.resources.ResourceLocation; + +import java.util.Collection; +import java.util.function.BiConsumer; + +/** + * A hook that is triggered when certain option flags are updated. + */ +public interface FlagHook extends BiConsumer, ConfigState> { + /** + * Gets the identifiers of the flags that trigger this hook. + * + * @return A collection of flag identifiers. + */ + Collection getTriggers(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java new file mode 100644 index 0000000000..be5149f1c4 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java @@ -0,0 +1,15 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import net.minecraft.network.chat.Component; + +/** + * Base interface extended by enums whose members can provide display names. + */ +public interface NameProvider { + /** + * Gets the display name of this item. + * + * @return the display name + */ + Component getName(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java new file mode 100644 index 0000000000..e1cf4038bc --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +/** + * Interface for binding an option to a storage mechanism. + * + * @param The type of the option value. + */ +public interface OptionBinding { + /** + * Saves the given value to the storage mechanism. + * + * @param value The value to save. + */ + void save(V value); + + /** + * Loads the value from the storage mechanism. + * + * @return The loaded value. + */ + V load(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java new file mode 100644 index 0000000000..f999c28a7f --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java @@ -0,0 +1,45 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Locale; + +/** + * Flags that indicate specific actions required when an option is changed. + */ +public enum OptionFlag { + /** + * Indicates that the renderer needs to be reloaded. This means all meshes will be discarded and rebuilt. This flag should only be applied if necessary, since rebuilding all meshes is a disruptive operation. + */ + REQUIRES_RENDERER_RELOAD, + + /** + * Indicates that the renderer needs to be updated. This means that the culling state may have changed and occlusion culling will be recalculated. This causes no noticeable disruption to the user. + */ + REQUIRES_RENDERER_UPDATE, + + /** + * Indicates that assets need to be reloaded. This reloads resource packs, and then reloads the renderer, causing all meshes to be discarded and rebuilt. + */ + REQUIRES_ASSET_RELOAD, + + /** + * Indicates that the video mode needs to be updated. This causes a disruption as the video mode is changed. + */ + REQUIRES_VIDEOMODE_RELOAD, + + /** + * Indicates that the game needs to be restarted for the option change to take effect. + */ + REQUIRES_GAME_RESTART; + + private final ResourceLocation id = ResourceLocation.fromNamespaceAndPath("sodium", "builtin_option_flag." + this.name().toLowerCase(Locale.ROOT)); + + /** + * Gets the {@link ResourceLocation} for this option flag. + * @return The identifier. + */ + public ResourceLocation getId() { + return this.id; + } +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java new file mode 100644 index 0000000000..0a7e591e5c --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java @@ -0,0 +1,41 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +/** + * Represents the performance impact level of a configuration option. + */ +public enum OptionImpact implements NameProvider { + /** + * Low impact on performance. Changing this option won't affect performance in a measurable or noticeable way. + */ + LOW(ChatFormatting.GREEN, "sodium.option_impact.low"), + + /** + * Medium impact on performance. Changing this option may have a noticeable effect on performance in some scenarios and some systems. + */ + MEDIUM(ChatFormatting.YELLOW, "sodium.option_impact.medium"), + + /** + * High impact on performance. Changing this option will likely have a significant effect on performance in most scenarios. + */ + HIGH(ChatFormatting.GOLD, "sodium.option_impact.high"), + + /** + * Varies in impact on performance. The effect of changing this option on performance is highly dependent on the specific scenario and system. + */ + VARIES(ChatFormatting.WHITE, "sodium.option_impact.varies"); + + private final Component text; + + OptionImpact(ChatFormatting formatting, String text) { + this.text = Component.translatable(text) + .withStyle(formatting); + } + + @Override + public Component getName() { + return this.text; + } +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java new file mode 100644 index 0000000000..e48662e7fc --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +/** + * A record representing a range of integer values with a specified step. When validating a value, it uses the default value if the value is out of range or does not conform to the step. + * + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @param step The step increment between valid values in the range. + */ +public record Range(int min, int max, int step) implements SteppedValidator { + public Range { + if (min > max) { + throw new IllegalArgumentException("Min must be less than or equal to max"); + } + if (step <= 0) { + throw new IllegalArgumentException("Step must be greater than 0"); + } + } +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java new file mode 100644 index 0000000000..9bbc5db47d --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java @@ -0,0 +1,46 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import java.util.function.Supplier; + +/** + * Common interface for validators that define a stepped range of integer values. + */ +public interface SteppedValidator extends Validator { + /** + * Gets the minimum value of the range. + * @return The minimum value. + */ + int min(); + + /** + * Gets the maximum value of the range. + * @return The maximum value. + */ + int max(); + + /** + * Gets the step increment between valid values in the range. + * + * @return The step increment. + */ + int step(); + + /** + * Checks if a given value is valid within this range. + * + * @param value The value to check. + * @return True if the value is valid, false otherwise. + */ + default boolean isValueValid(int value) { + int min = this.min(); + return value >= min && value <= this.max() && (value - min) % this.step() == 0; + } + + @Override + default Integer getValidatedValue(Integer value, Supplier defaultValueSupplier) { + if (this.isValueValid(value)) { + return value; + } + return defaultValueSupplier.get(); + } +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java new file mode 100644 index 0000000000..b96e033b03 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.api.config.option; + +import java.util.function.Supplier; + +/** + * Common interface for validators that validate values of type V. + * + * @param The type of value to be validated. + */ +public interface Validator { + V getValidatedValue(V value, Supplier defaultValueSupplier); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java new file mode 100644 index 0000000000..f59c5143fc --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java @@ -0,0 +1,63 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Builder interface for defining boolean options. Refines builder methods to return this class instead of the base interface and have a {@link Boolean} value type. + */ +public interface BooleanOptionBuilder extends StatefulOptionBuilder { + @Override + BooleanOptionBuilder setName(Component name); + + @Override + BooleanOptionBuilder setEnabled(boolean available); + + @Override + BooleanOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); + + @Override + BooleanOptionBuilder setStorageHandler(StorageEventHandler storage); + + @Override + BooleanOptionBuilder setTooltip(Component tooltip); + + @Override + BooleanOptionBuilder setTooltip(Function tooltip); + + @Override + BooleanOptionBuilder setImpact(OptionImpact impact); + + @Override + BooleanOptionBuilder setFlags(OptionFlag... flags); + + @Override + BooleanOptionBuilder setFlags(ResourceLocation... flags); + + @Override + BooleanOptionBuilder setDefaultValue(Boolean value); + + @Override + BooleanOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies); + + @Override + BooleanOptionBuilder setControlHiddenWhenDisabled(boolean hidden); + + @Override + BooleanOptionBuilder setBinding(Consumer save, Supplier load); + + @Override + BooleanOptionBuilder setBinding(OptionBinding binding); + + @Override + BooleanOptionBuilder setApplyHook(Consumer hook); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java new file mode 100644 index 0000000000..546421110b --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java @@ -0,0 +1,26 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +/** + * Builder interface for defining color themes. + *

+ * Colors use RGB integers (bits 0 to 24). This is ARGB format with alpha bits ignored. + */ +public interface ColorThemeBuilder { + /** + * Sets the base theme color. + * + * @param theme Theme color as an RGB integer. + * @return The current builder instance. + */ + ColorThemeBuilder setBaseThemeRGB(int theme); + + /** + * Sets the full theme colors. + * + * @param theme Theme color as an RGB integer. + * @param themeHighlight Theme highlight color as an RGB integer. + * @param themeDisabled Theme disabled color as an RGB integer. + * @return The current builder instance. + */ + ColorThemeBuilder setFullThemeRGB(int theme, int themeHighlight, int themeDisabled); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java new file mode 100644 index 0000000000..da258aba8d --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java @@ -0,0 +1,97 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.minecraft.resources.ResourceLocation; + +/** + * Root builder interface for defining configuration structures. An implementation of this class is passed to your entry point and lets you create builders for everything in the API. Builders do not need to be built manually, as they are automatically closed when the enclosing scope ends. + *

+ * Refer to USAGE.md for usage instructions. + */ +public interface ConfigBuilder { + /** + * Registers a new mod options structure with the given configId, name, and version. Note that a mod may register multiple mod options structures under different configIds if desired. + * + * @param configId The configId of these mod options. + * @param name The human-readable name of the mod. + * @param version The version of the mod. + * @return A builder for defining the mod's configuration structure. + */ + ModOptionsBuilder registerModOptions(String configId, String name, String version); + + /** + * Registers a new mod options structure for the mod with the given configId. The mod's name and version will be looked up automatically. + * + * @param configId The configId of these mod options. + * @return A builder for defining the mod's configuration structure. + */ + ModOptionsBuilder registerModOptions(String configId); + + /** + * Registers a new mod options structure for the mod of the current entrypoint. The mod's name and version will be looked up automatically. + * + * @return A builder for defining the mod's configuration structure. + */ + ModOptionsBuilder registerOwnModOptions(); + + /** + * Creates a new color theme builder. + * + * @return A builder for defining a color theme. + */ + ColorThemeBuilder createColorTheme(); + + /** + * Creates a new option page builder. + * + * @return A builder for defining an option page. + */ + OptionPageBuilder createOptionPage(); + + /** + * Creates a new external page builder. + * + * @return A builder for defining an external page. + */ + ExternalPageBuilder createExternalPage(); + + /** + * Creates a new option group builder. + * + * @return A builder for defining an option group. + */ + OptionGroupBuilder createOptionGroup(); + + /** + * Creates a new boolean option builder. + * + * @param id The unique identifier for this option. + * @return A builder for defining a boolean option. + */ + BooleanOptionBuilder createBooleanOption(ResourceLocation id); + + /** + * Creates a new integer option builder. + * + * @param id The unique identifier for this option. + * @return A builder for defining an integer option. + */ + IntegerOptionBuilder createIntegerOption(ResourceLocation id); + + /** + * Creates a new enum option builder. + * + * @param id The unique identifier for this option. + * @param enumClass The enum class for this option. + * @param The enum type. + * @return A builder for defining an enum option. + */ + > EnumOptionBuilder createEnumOption(ResourceLocation id, Class enumClass); + + /** + * Creates a new external button option builder. + * + * @param id The unique identifier for this option. + * @return A builder for defining an external button option. + */ + ExternalButtonOptionBuilder createExternalButtonOption(ResourceLocation id); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java new file mode 100644 index 0000000000..06ccaf7f56 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java @@ -0,0 +1,102 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Builder interface for defining enum options. Refines builder methods to return this class instead of the base interface and have an {@link Enum} value type. + * + * @param The enum type for this option. + */ +public interface EnumOptionBuilder> extends StatefulOptionBuilder { + /** + * Creates a name provider function that maps enum constants to the provided names based on their ordinal values. + * + * @param names The array of names corresponding to the enum constants. + * @param The enum type. + * @return A function that provides names for enum constants. + */ + static > Function nameProviderFrom(Component... names) { + return e -> names[e.ordinal()]; + } + + @Override + EnumOptionBuilder setName(Component name); + + @Override + EnumOptionBuilder setEnabled(boolean available); + + @Override + EnumOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); + + @Override + EnumOptionBuilder setStorageHandler(StorageEventHandler storage); + + @Override + EnumOptionBuilder setTooltip(Component tooltip); + + @Override + EnumOptionBuilder setTooltip(Function tooltip); + + @Override + EnumOptionBuilder setImpact(OptionImpact impact); + + @Override + EnumOptionBuilder setFlags(OptionFlag... flags); + + @Override + EnumOptionBuilder setFlags(ResourceLocation... flags); + + @Override + EnumOptionBuilder setDefaultValue(E value); + + @Override + EnumOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies); + + @Override + EnumOptionBuilder setControlHiddenWhenDisabled(boolean hidden); + + @Override + EnumOptionBuilder setBinding(Consumer save, Supplier load); + + @Override + EnumOptionBuilder setBinding(OptionBinding binding); + + @Override + EnumOptionBuilder setApplyHook(Consumer hook); + + /** + * Sets the allowed values for this enum option. + * + * @param allowedValues The set of allowed enum values. + * @return This builder instance. + */ + EnumOptionBuilder setAllowedValues(Set allowedValues); + + /** + * Sets a provider function to determine the allowed values for this enum option based on the current configuration state. + * + * @param provider The function that provides the set of allowed enum values. + * @param dependencies The options that this provider depends on. + * @return This builder instance. + */ + EnumOptionBuilder setAllowedValuesProvider(Function> provider, ResourceLocation... dependencies); + + /** + * Sets a provider function to determine the display name for each enum constant. + * + * @param provider The function that provides the display name for each enum constant. + * @return This builder instance. + */ + EnumOptionBuilder setElementNameProvider(Function provider); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java new file mode 100644 index 0000000000..257c7d56fc --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java @@ -0,0 +1,33 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Builder interface for defining external button options. + */ +public interface ExternalButtonOptionBuilder extends OptionBuilder { + @Override + ExternalButtonOptionBuilder setName(Component name); + + @Override + ExternalButtonOptionBuilder setTooltip(Component tooltip); + + @Override + ExternalButtonOptionBuilder setEnabled(boolean available); + + @Override + ExternalButtonOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); + + /** Sets the screen consumer for the external button option. + * + * @param currentScreenConsumer A consumer that accepts the current screen and opens the external configuration screen. + * @return The current builder instance. + */ + ExternalButtonOptionBuilder setScreenConsumer(Consumer currentScreenConsumer); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java new file mode 100644 index 0000000000..198ebff7e8 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +/** + * Builder interface for defining external configuration pages. + */ +public interface ExternalPageBuilder extends PageBuilder { + /** + * Sets the name of the external configuration page. + * + * @param name The name component. + * @return The current builder instance. + */ + ExternalPageBuilder setName(Component name); + + /** + * Sets the screen provider for the external configuration page. + * + * @param currentScreenConsumer A consumer that accepts the current screen and opens the external configuration screen. + * @return The current builder instance. + */ + ExternalPageBuilder setScreenConsumer(Consumer currentScreenConsumer); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java new file mode 100644 index 0000000000..85965a8508 --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java @@ -0,0 +1,113 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.*; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Builder interface for defining integer options. Refines builder methods to return this class instead of the base interface and have an {@link Integer} value type. + */ +public interface IntegerOptionBuilder extends StatefulOptionBuilder { + @Override + IntegerOptionBuilder setName(Component name); + + @Override + IntegerOptionBuilder setEnabled(boolean available); + + @Override + IntegerOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); + + @Override + IntegerOptionBuilder setStorageHandler(StorageEventHandler storage); + + @Override + IntegerOptionBuilder setTooltip(Component tooltip); + + @Override + IntegerOptionBuilder setTooltip(Function tooltip); + + @Override + IntegerOptionBuilder setImpact(OptionImpact impact); + + @Override + IntegerOptionBuilder setFlags(OptionFlag... flags); + + @Override + IntegerOptionBuilder setFlags(ResourceLocation... flags); + + @Override + IntegerOptionBuilder setDefaultValue(Integer value); + + @Override + IntegerOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies); + + @Override + IntegerOptionBuilder setControlHiddenWhenDisabled(boolean hidden); + + @Override + IntegerOptionBuilder setBinding(Consumer save, Supplier load); + + @Override + IntegerOptionBuilder setBinding(OptionBinding binding); + + @Override + IntegerOptionBuilder setApplyHook(Consumer hook); + + /** + * Sets the range for this integer option. + * + * @param min The minimum value (inclusive). + * @param max The maximum value (inclusive). + * @param step The step value for increments. + * @return The current builder instance. + */ + IntegerOptionBuilder setRange(int min, int max, int step); + + /** + * Sets the range for this integer option. + * + * @param range The range object defining min, max, and step. + * @return The current builder instance. + */ + IntegerOptionBuilder setRange(Range range); + + /** + * Sets a provider function to determine the range for this integer option based on the current configuration state. + * + * @param provider The function that provides the range. + * @param dependencies The options that this provider depends on. + * @return The current builder instance. + */ + IntegerOptionBuilder setRangeProvider(Function provider, ResourceLocation... dependencies); + + /** + * Sets a validator for this integer option. A {@link Range} is a type of stepped validator. + * + * @param validator The validator to set. + * @return The current builder instance. + */ + IntegerOptionBuilder setValidator(SteppedValidator validator); + + /** + * Sets a provider function to determine the validator for this integer option based on the current configuration state. + * + * @param provider The function that provides the validator. + * @param dependencies The options that this provider depends on. + * @return The current builder instance. + */ + IntegerOptionBuilder setValidatorProvider(Function provider, ResourceLocation... dependencies); + + /** + * Sets the value formatter for this integer option. + * + * @param formatter The formatter to format the integer value of this option. + * @return The current builder instance. + */ + IntegerOptionBuilder setValueFormatter(ControlValueFormatter formatter); +} \ No newline at end of file diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java new file mode 100644 index 0000000000..d4e8d6e76c --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java @@ -0,0 +1,123 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.option.FlagHook; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.minecraft.resources.ResourceLocation; + +import java.util.Collection; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Builder interface for defining options belonging to a mod and its metadata. A set of mod options contains some list of option pages, option overrides, and option overlays. At least one page, override, or overlay must be defined. + */ +public interface ModOptionsBuilder { + /** + * Sets the display name of the mod. + * + * @param name The mod name. + * @return The current builder instance. + */ + ModOptionsBuilder setName(String name); + + /** + * Sets the version string of the mod. This value is typically automatically populated from the mod metadata known to the mod loader. + * + * @param version The version string. + * @return The current builder instance. + */ + ModOptionsBuilder setVersion(String version); + + /** + * Sets a formatter function for the mod version string. This converts from the raw version string to a display version string. + * + * @param versionFormatter The version formatter function. + * @return The current builder instance. + */ + ModOptionsBuilder formatVersion(Function versionFormatter); + + /** + * Sets the color theme for the mod options UI. A color theme is optional and a theme will be chosen at random (deterministically) from a predetermined set of reasonable colors if none is provided. + *

+ * Basic validation is applied to the color theme to ensure it's not grayscale and not too dark. + * + * @param colorTheme The color theme builder. + * @return The current builder instance. + */ + ModOptionsBuilder setColorTheme(ColorThemeBuilder colorTheme); + + /** + * Sets the icon texture for the mod. See the documentation for more information about appropriate icon design. The summary is as follows. An icon should: + * + *

    + *
  • Be a square texture (e.g. 64x64, 128x128, etc).
  • + *
  • Have a transparent background unless the main content of the icon fills the entire square.
  • + *
  • Have binary alpha (fully opaque or fully transparent).
  • + *
  • Have limited detail since it will be rendered at a small size.
  • + *
+ *

+ * Icons set with this method are tinted monochrome in the mod's theme color. This means the texture itself should be fully white to result in the theme when tinted. + *

+ * No icon will be shown if none is provided and the layout adjusted accordingly. + * + * @param texture The ID of the icon texture. + * @return The current builder instance. + */ + ModOptionsBuilder setIcon(ResourceLocation texture); + + /** + * Sets the icon texture for the mod. Same as {@link #setIcon(ResourceLocation)}, but the texture will be rendered in its original color instead of being tinted. + * + * @param texture The ID of the icon texture. + * @return The current builder instance. + */ + ModOptionsBuilder setNonTintedIcon(ResourceLocation texture); + + /** + * Adds a configuration page to the mod options. + * + * @param page The page builder. + * @return The current builder instance. + */ + ModOptionsBuilder addPage(PageBuilder page); + + /** + * Registers an option override provided by this mod. Overrides allow modifying the behavior or appearance of options defined by other mods. + *

+ * The ID of the provided replacement option can match the original option to allow other mods to apply overlays targeting the original option ID. If the replacement option has a different ID, overlays must target the new ID. + * + * @param target The ID of the option to override. + * @param replacement The option builder that defines the replacement option. + * @return The current builder instance. + */ + ModOptionsBuilder registerOptionReplacement(ResourceLocation target, OptionBuilder replacement); + + /** + * Registers an option overlay provided by this mod. Overlays allow partially changing an option instead of replacing it entirely. + *

+ * The target option ID must match the ID of an existing option, either defined by another mod or by a replacement option defined by this mod. If the target option has been replaced, overlays must target the ID of the replacement option, which may or may not be the same as the original option ID. + * + * @param target The ID of the option to overlay. + * @param overlay The option builder that defines the overlay changes. + * @return The current builder instance. + */ + ModOptionsBuilder registerOptionOverlay(ResourceLocation target, OptionBuilder overlay); + + /** + * Registers a hook that will be called after an option which has any of the specified flags changed. This can be used to implement custom behavior in response to option changes. To hook on built-in flags, use the identifiers given by {@link OptionFlag#getId()}. The hook is given an array of all flags that triggered the hook. Note that the hook may be called with a set of flags larger than the set of flags it is interested in for performance reasons, since this lets us avoid generating a different flag set for every hook. + * + * @param hook The hook to run, with the set of all triggered flags. + * @param triggers The flags to listen for. + * @return The current builder instance. + */ + ModOptionsBuilder registerFlagHook(BiConsumer, ConfigState> hook, ResourceLocation... triggers); + + /** + * Registers a hook just like {@link #registerFlagHook(BiConsumer, ResourceLocation...)}, but using a {@link FlagHook}. + * + * @param hook The flag hook to register. + * @return The current builder instance. + */ + ModOptionsBuilder registerFlagHook(FlagHook hook); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java new file mode 100644 index 0000000000..e07e734d7b --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java @@ -0,0 +1,45 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Function; + +/** + * The base interface for option builders. + */ +public interface OptionBuilder { + /** + * Sets the name of the option. + * + * @param name The display name of the option. + * @return The current builder instance. + */ + OptionBuilder setName(Component name); + + /** + * Sets the tooltip of the option. + * + * @param tooltip The tooltip component. + * @return The current builder instance. + */ + OptionBuilder setTooltip(Component tooltip); + + /** + * Sets whether the option is enabled. + * + * @param available True if the option is enabled, false otherwise. + * @return The current builder instance. + */ + OptionBuilder setEnabled(boolean available); + + /** + * Sets a provider function to determine whether the option is enabled based on the current configuration state. + * + * @param provider The function that provides the enabled state. + * @param dependencies The options that this provider depends on. + * @return The current builder instance. + */ + OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java new file mode 100644 index 0000000000..71feff80ae --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java @@ -0,0 +1,26 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.minecraft.network.chat.Component; + +/** + * Builder interface for defining option groups, which are collections of related options. + *

+ * Option groups only exist to visually group options together in the configuration UI, and a title is not required and won't be displayed unless set. + */ +public interface OptionGroupBuilder { + /** + * Sets the name of this option group. This is optional and won't be displayed unless set. We recommend only using this if necessary, as usually option groups without names are enough grouping and option names provide sufficiently detailed labels of the option categories. + * + * @param name This option group's display name. + * @return The current builder instance. + */ + OptionGroupBuilder setName(Component name); + + /** + * Adds an option to this option group. + * + * @param option The option to add. + * @return The current builder instance. + */ + OptionGroupBuilder addOption(OptionBuilder option); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java new file mode 100644 index 0000000000..48d27a8fdf --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java @@ -0,0 +1,32 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.minecraft.network.chat.Component; + +/** + * Builder interface for defining option pages, which are lists of option groups. + */ +public interface OptionPageBuilder extends PageBuilder { + /** + * Sets the name of the option page. + * + * @param name The name component. + * @return The current builder instance. + */ + OptionPageBuilder setName(Component name); + + /** + * Adds an option group to the option page. + * + * @param group The option group builder. + * @return The current builder instance. + */ + OptionPageBuilder addOptionGroup(OptionGroupBuilder group); + + /** + * Adds an option directly to the option page. Options added directly to the page are grouped into an implicit unnamed option group at the bottom of the page. + * + * @param option The option to add. + * @return The current builder instance. + */ + OptionPageBuilder addOption(OptionBuilder option); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java new file mode 100644 index 0000000000..3fa2370c2f --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +/** + * Base interface for all option page builders. + */ +public interface PageBuilder { +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java new file mode 100644 index 0000000000..60d5ef010d --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java @@ -0,0 +1,124 @@ +package net.caffeinemc.mods.sodium.api.config.structure; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Builder interface for defining stateful options. + * + * @param The type of the option's value. + */ +public interface StatefulOptionBuilder extends OptionBuilder { + @Override + StatefulOptionBuilder setName(Component name); + + @Override + OptionBuilder setEnabled(boolean available); + + @Override + OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies); + + /** + * Sets the storage handler for this option. + * + * @param storage The storage event handler. + * @return The current builder instance. + */ + StatefulOptionBuilder setStorageHandler(StorageEventHandler storage); + + @Override + StatefulOptionBuilder setTooltip(Component tooltip); + + /** + * Sets a functional tooltip for this option that changes the text based on the selected value. + * + * @param tooltip The function that provides the tooltip based on the option's value. + * @return The current builder instance. + */ + StatefulOptionBuilder setTooltip(Function tooltip); + + /** + * Sets the performance impact level of this option. + * + * @param impact The option's performance impact. + * @return The current builder instance. + */ + StatefulOptionBuilder setImpact(OptionImpact impact); + + /** + * Sets flags for this option. + * + * @param flags The option flags. + * @return The current builder instance. + */ + StatefulOptionBuilder setFlags(OptionFlag... flags); + + /** + * Sets flags for this option using {@link ResourceLocation} instances. + * + * @param flags The flags as identifiers. + * @return The current builder instance. + */ + StatefulOptionBuilder setFlags(ResourceLocation... flags); + + /** + * Sets the default value for this option. The default value is used when the binding returns an invalid value, such as during the first load. + * + * @param value The default value. + * @return The current builder instance. + */ + StatefulOptionBuilder setDefaultValue(V value); + + /** + * Sets a provider function to determine the default value for this option based on the current configuration state. + * + * @param provider The function that provides the default value. + * @param dependencies The options that this provider depends on. + * @return The current builder instance. + */ + StatefulOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies); + + /** + * Sets whether the control for this option should be hidden when the option is disabled. This should only be set to false when the user should know what the state of the option is even when it is disabled, and they cannot interact with it. + *

+ * By default, controls are hidden when disabled. + * + * @param hidden True to hide the control when disabled, false to show it. + * @return The current builder instance. + */ + StatefulOptionBuilder setControlHiddenWhenDisabled(boolean hidden); + + /** + * Sets a binding for this option using save and load functions. + * + * @param save The function to save the option's value. + * @param load The function to load the option's value. + * @return The current builder instance. + */ + StatefulOptionBuilder setBinding(Consumer save, Supplier load); + + /** + * Sets a binding for this option using an {@link OptionBinding} instance. + * + * @param binding The option binding. + * @return The current builder instance. + */ + StatefulOptionBuilder setBinding(OptionBinding binding); + + /** + * Sets a hook that is triggered after the options' value has been saved if it changed. + * + * @param hook The hook to be executed after applying the option. + * @return The current builder instance. + */ + StatefulOptionBuilder setApplyHook(Consumer hook); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java index df8806b7f2..9d2bb6920d 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java @@ -10,7 +10,7 @@ /** * Implements optimized utilities for transforming vectors with a given matrix. - * + *

* Note: Brackets must be used carefully in the transform functions to ensure that floating-point errors are * the same as those produced by JOML, otherwise Z-fighting will occur. */ diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java new file mode 100644 index 0000000000..4f33c26a7f --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java @@ -0,0 +1,31 @@ +package net.caffeinemc.mods.sodium.api.texture; + +import net.caffeinemc.mods.sodium.api.internal.DependencyInjection; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Utility functions for querying sprite information and updating per-frame information about sprite visibility. + */ +@ApiStatus.Experimental +public interface SpriteUtil { + SpriteUtil INSTANCE = DependencyInjection.load(SpriteUtil.class, + "net.caffeinemc.mods.sodium.client.render.texture.SpriteUtilImpl"); + + /** + * Marks the sprite as "active", meaning that it is visible during this frame and should have the animation + * state updated. Mods which perform their own rendering without the use of Minecraft's helpers will need to + * call this method once every frame, when their sprite is actively being used in rendering. + * @param sprite The sprite to mark as active + */ + void markSpriteActive(@NotNull TextureAtlasSprite sprite); + + /** + * Returns if the provided sprite has an animation. + * + * @param sprite The sprite to query an animation for + * @return {@code true} if the provided sprite has an animation, otherwise {@code false} + */ + boolean hasAnimation(@NotNull TextureAtlasSprite sprite); +} \ No newline at end of file diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java index a7e86c52c9..b9e4437cd4 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java @@ -1,19 +1,26 @@ package net.caffeinemc.mods.sodium.api.util; +import java.nio.ByteOrder; + /** * 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 { - private static final int RED_COMPONENT_OFFSET = 0; + private static final int RED_COMPONENT_OFFSET = 0; private static final int GREEN_COMPONENT_OFFSET = 8; - private static final int BLUE_COMPONENT_OFFSET = 16; + private static final int BLUE_COMPONENT_OFFSET = 16; private static final int ALPHA_COMPONENT_OFFSET = 24; + private static final int RED_COMPONENT_MASK = COMPONENT_MASK << RED_COMPONENT_OFFSET; + private static final int GREEN_COMPONENT_MASK = COMPONENT_MASK << GREEN_COMPONENT_OFFSET; + private static final int BLUE_COMPONENT_MASK = COMPONENT_MASK << BLUE_COMPONENT_OFFSET; + private static final int ALPHA_COMPONENT_MASK = COMPONENT_MASK << ALPHA_COMPONENT_OFFSET; + /** * Packs the specified color components into ABGR format. The alpha component is fully opaque. * @param r The red component of the color @@ -98,4 +105,51 @@ public static int unpackBlue(int color) { public static int unpackAlpha(int color) { return (color >> ALPHA_COMPONENT_OFFSET) & COMPONENT_MASK; } + + /** + * Multiplies the RGB components of the color with the provided factor. The alpha component is not modified. + * + * @param color The packed 32-bit ABGR color to be multiplied + * @param factor The darkening factor (in the range of 0..255) to multiply with + */ + public static int mulRGB(int color, int factor) { + return (ColorMixer.mul(color, factor) & ~ALPHA_COMPONENT_MASK) | (color & ALPHA_COMPONENT_MASK); + } + + /** + * See {@link #mulRGB(int, int)}. This function is identical, but it accepts a float in [0.0, 1.0] instead, which + * is then mapped to [0, 255]. + * + * @param color The packed 32-bit ABGR color to be multiplied + * @param factor The darkening factor (in the range of 0.0..1.0) to multiply with + */ + public static int mulRGB(int color, float factor) { + return mulRGB(color, ColorU8.normalizedFloatToByte(factor)); + } + + private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; + + /** + * Shuffles the ordering of the ABGR color so that it can then be written out to memory in the platform's native + * byte ordering. This should be used when writing the packed color to memory (in native-order) as a 32-bit word. + */ + public static int fromNativeByteOrder(int color) { + if (BIG_ENDIAN) { + return Integer.reverseBytes(color); + } else { + return color; + } + } + + /** + * Shuffles the ordering of the ABGR color from the platform's native byte ordering. This should be used when reading + * the packed color from memory (in native-order) as a 32-bit word. + */ + public static int toNativeByteOrder(int color) { + if (BIG_ENDIAN) { + return Integer.reverseBytes(color); + } else { + return color; + } + } } diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java index f93880555e..8786262a31 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java @@ -4,7 +4,7 @@ * 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 | @@ -15,8 +15,14 @@ public class ColorARGB implements ColorU8 { private static final int GREEN_COMPONENT_OFFSET = 8; private static final int BLUE_COMPONENT_OFFSET = 0; + private static final int RED_COMPONENT_MASK = COMPONENT_MASK << RED_COMPONENT_OFFSET; + private static final int GREEN_COMPONENT_MASK = COMPONENT_MASK << GREEN_COMPONENT_OFFSET; + private static final int BLUE_COMPONENT_MASK = COMPONENT_MASK << BLUE_COMPONENT_OFFSET; + private static final int ALPHA_COMPONENT_MASK = COMPONENT_MASK << ALPHA_COMPONENT_OFFSET; + /** * 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 @@ -32,6 +38,7 @@ public static int pack(int r, int g, int b, int a) { /** * Packs the specified color components into big-endian format for consumption by OpenGL. The alpha * channel is fully opaque. + * * @param r The red component of the color * @param g The green component of the color * @param b The blue component of the color @@ -73,29 +80,155 @@ public static int unpackBlue(int color) { } /** - * Re-packs the ARGB color into an ABGR color with the specified alpha component. + * Swizzles from ARGB format into ABGR format, replacing the alpha component with {@param alpha}. */ - public static int toABGR(int color, float alpha) { - return Integer.reverseBytes(color << 8 | ColorU8.normalizedFloatToByte(alpha)); + public static int toABGR(int color, int alpha) { + // shl(ARGB, 8) -> RGB0 + // or(RGB0, 000A) -> RGBA + return Integer.reverseBytes(color << 8 | alpha); } /** - * Re-packs the ARGB color into a aBGR color with the specified alpha component. + * Swizzles from ARGB format into ABGR format, replacing the alpha component with {@param alpha}. The alpha + * component is mapped from [0.0, 1.0] to [0, 255]. */ - public static int toABGR(int color, int alpha) { - return Integer.reverseBytes(color << 8 | alpha); + public static int toABGR(int color, float alpha) { + return toABGR(color, ColorU8.normalizedFloatToByte(alpha)); } + /** + * Swizzles from ARGB format into ABGR format. + */ public static int toABGR(int color) { + // rotateLeft(ARGB, 8) -> RGBA + // reverseBytes(RGBA) -> ABGR return Integer.reverseBytes(Integer.rotateLeft(color, 8)); } + /** + * Swizzles from ABGR format into ARGB format. + */ + public static int fromABGR(int color) { + // reverseBytes(ABGR) -> RGBA + // rotateRight(RGBA, 8) -> ARGB + return Integer.rotateRight(Integer.reverseBytes(color), 8); + } + /** * Packs the specified color components into ARGB format. - * @param rgb The red/green/blue component of the color + * + * @param rgb The red/green/blue component of the color * @param alpha The alpha component of the color */ public static int withAlpha(int rgb, int alpha) { return (alpha << ALPHA_COMPONENT_OFFSET) | (rgb & ~(COMPONENT_MASK << ALPHA_COMPONENT_OFFSET)); } + + /** + * Replaces the alpha component of the specified color with the alpha component of another color. + * + * @param color The packed 32-bit ARGB color to modify + * @param alphaColor The packed 32-bit ARGB color to take the alpha component from + * @return The modified packed 32-bit ARGB color + */ + public static int transferAlpha(int color, int alphaColor) { + return withAlpha(color, unpackAlpha(alphaColor)); + } + + /** + * Multiplies the RGB components of the color with the provided factor. The alpha component is not modified. + * + * @param color The packed 32-bit ABGR color to be multiplied + * @param factor The darkening factor (in the range of 0..255) to multiply with + */ + public static int mulRGB(int color, int factor) { + return (ColorMixer.mul(color, factor) & ~ALPHA_COMPONENT_MASK) | (color & ALPHA_COMPONENT_MASK); + } + + /** + * See {@link #mulRGB(int, int)}. This function is identical, but it accepts a float in [0.0, 1.0] instead, which + * is then mapped to [0, 255]. + * + * @param color The packed 32-bit ABGR color to be multiplied + * @param factor The darkening factor (in the range of 0.0..1.0) to multiply with + */ + public static int mulRGB(int color, float factor) { + return mulRGB(color, ColorU8.normalizedFloatToByte(factor)); + } + + /** + * Converts the specified packed ARGB color into HSV color space. + *

+ * Used with permission from patbox. + * + * @param color The packed 32-bit ARGB color to convert + * @return An array containing the hue, saturation and value components in [0,1] in that order. + */ + public static float[] toHSV(int color) { + float r = (float) unpackRed(color) / 255; + float g = (float) unpackGreen(color) / 255; + float b = (float) unpackBlue(color) / 255; + + float cmax = Math.max(r, Math.max(g, b)); + float cmin = Math.min(r, Math.min(g, b)); + float diff = cmax - cmin; + float h = -1, s = -1; + + if (cmax == cmin) { + h = 0; + } else if (cmax == r) { + h = (0.1666f * ((g - b) / diff) + 1) % 1; + } else if (cmax == g) { + h = (0.1666f * ((b - r) / diff) + 0.333f) % 1; + } else if (cmax == b) { + h = (0.1666f * ((r - g) / diff) + 0.666f) % 1; + } + if (cmax == 0) { + s = 0; + } else { + s = (diff / cmax); + } + + return new float[] { h, s, cmax }; + } + + private static int pack(float r, float g, float b) { + return pack( + ColorU8.normalizedFloatToByte(r), + ColorU8.normalizedFloatToByte(g), + ColorU8.normalizedFloatToByte(b) + ); + } + + /** + * Converts the specified HSV color components into a packed ARGB color. + *

+ * Used with permission from patbox + * + * @param hue The hue component in [0,1] + * @param saturation The saturation component in [0,1] + * @param value The value component in [0,1] + * @return The packed 32-bit ARGB color + */ + public static int fromHSV(float hue, float saturation, float value) { + int h = (int) (hue * 6) % 6; + float f = hue * 6 - h; + float p = value * (1 - saturation); + float q = value * (1 - f * saturation); + float t = value * (1 - (1 - f) * saturation); + + return switch (h) { + case 0 -> pack(value, t, p); + case 1 -> pack(q, value, p); + case 2 -> pack(p, value, t); + case 3 -> pack(p, q, value); + case 4 -> pack(t, p, value); + case 5 -> pack(value, p, q); + default -> 0; + }; + } + + public static int fromHSV(float[] hsv) { + return fromHSV(hsv[0], hsv[1], hsv[2]); + } } diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java index a1934c8b44..220eae17ca 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java @@ -1,77 +1,146 @@ package net.caffeinemc.mods.sodium.api.util; +import org.jetbrains.annotations.ApiStatus; + +/** + * A collection of optimized color mixing functions which directly operate on packed color values. These functions are + * agnostic to the ordering of color channels, and the output value will always use the same channel ordering as + * the input values. + */ public class ColorMixer { - private static final int CHANNEL_MASK = 0x00FF00FF; + /** + *

Linearly interpolate between the {@param start} and {@param end} points, represented as packed unsigned 8-bit + * values within a 32-bit integer. The result is computed as

(start * weight) + (end * (255 - weight))
+ * using fixed-point arithmetic and round-to-nearest behavior.

+ * + *

The results are undefined if {@param weight} is not within the interval [0, 255].

+ * + *

If {@param start} and {@param end} are the same value, the result of this function will always be that value, + * regardless of {@param weight}.

+ * + * @param start The value at the start of the range to interpolate + * @param end The value at the end of the range to interpolate + * @param weight The weight value used to interpolate between color values (in 0..255 range) + * @return The color that was interpolated between the start and end points + */ + public static int mix(int start, int end, int weight) { + // De-interleave the 8-bit component lanes into high and low halves for each point. + // Multiply the start point by alpha, and the end point by 1-alpha, to produce Q8.8 fixed-point intermediates. + // Add the Q8.8 fixed-point intermediaries together to obtain the mixed values. + final long hi = ((start & 0x00FF00FFL) * weight) + ((end & 0x00FF00FFL) * (ColorU8.COMPONENT_MASK - weight)); + final long lo = ((start & 0xFF00FF00L) * weight) + ((end & 0xFF00FF00L) * (ColorU8.COMPONENT_MASK - weight)); + + // Round the fixed-point values to the nearest integer, and interleave the high and low halves to + // produce the final packed result. + final long result = + (((hi + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) | + (((lo + 0xFF00FF00L) >>> 8) & 0xFF00FF00L); + + return (int) result; + } /** - * Mixes a 32-bit color (with packed 8-bit components) into another using the given ratio. This is equivalent to - * (color1 * ratio) + (color2 * (1.0 - ratio)) but uses bitwise trickery for maximum performance. - *

- * The order of the channels within the packed color does not matter, and the output color will always - * have the same ordering as the input colors. + *

This function is identical to {@link ColorMixer#mix(int, int, int)}, but {@param weight} is a normalized + * floating-point value within the interval of [0.0, 1.0].

+ * + *

The results are undefined if {@param weight} is not within the interval [0.0, 1.0].

* - * @param aColor The color to mix towards - * @param bColor The color to mix away from - * @param ratio The percentage (in 0.0..1.0 range) to mix the first color into the second color - * @return The mixed color in packed 32-bit format + * @param start The start of the range to interpolate + * @param end The end of the range to interpolate + * @param weight The weight value used to interpolate between color values (in 0.0..1.0 range) + * @return The color that was interpolated between the start and end points */ - public static int mix(int aColor, int bColor, float ratio) { - int aRatio = (int) (256 * ratio); // int(ratio) - int bRatio = 256 - aRatio; // int(1.0 - ratio) - - // Mask off and shift two components from each color into a packed vector of 16-bit components, where the - // high 8 bits are all zeroes. - int a1 = (aColor >> 0) & CHANNEL_MASK; - int b1 = (bColor >> 0) & CHANNEL_MASK; - int a2 = (aColor >> 8) & CHANNEL_MASK; - int b2 = (bColor >> 8) & CHANNEL_MASK; - - // Multiply the packed 16-bit components against each mix factor, and add the components of each color - // to produce the mixed result. This will never overflow since both 16-bit integers are in 0..255 range. - - // Then, shift the high 8 bits of each packed 16-bit component into the low 8 bits, and mask off the high bits of - // each 16-bit component to produce a vector of packed 8-bit components, where every other component is empty. - int c1 = (((a1 * aRatio) + (b1 * bRatio)) >> 8) & CHANNEL_MASK; - int c2 = (((a2 * aRatio) + (b2 * bRatio)) >> 8) & CHANNEL_MASK; - - // Join the color components into the original order - return ((c1 << 0) | (c2 << 8)); + public static int mix(int start, int end, float weight) { + return mix(start, end, ColorU8.normalizedFloatToByte(weight)); } /** - * Multiplies the 32-bit colors (with packed 8-bit components) together. - *

- * The order of the channels within the packed color does not matter, and the output color will always - * have the same ordering as the input colors. + *

Performs bi-linear interpolation on a 2x2 matrix of color values to derive the point (x, y). This is more + * efficient than chaining {@link #mul(int, float)} calls.

* - * @param a The first color to multiply - * @param b The second color to multiply - * @return The multiplied color in packed 32-bit format + *

The results are undefined if {@param x} and {@param y} are not within the interval [0.0, 1.0].

+ * + * @param m00 The packed color value for (0, 0) + * @param m01 The packed color value for (0, 1) + * @param m10 The packed color value for (1, 0) + * @param m11 The packed color value for (1, 1) + * @param x The amount to interpolate between x=0 and x=1 + * @param y The amount to interpolate between y=0 and y=1 + * @return The interpolated color value */ - public static int mul(int a, int b) { - // Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers, - // and then shift the high half of each 16-bit integer into 8-bit integers. - int c0 = (((a >> 0) & 0xFF) * ((b >> 0) & 0xFF)) >> 8; - int c1 = (((a >> 8) & 0xFF) * ((b >> 8) & 0xFF)) >> 8; - int c2 = (((a >> 16) & 0xFF) * ((b >> 16) & 0xFF)) >> 8; - int c3 = (((a >> 24) & 0xFF) * ((b >> 24) & 0xFF)) >> 8; - - // Pack the components - return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24); + @ApiStatus.Experimental + public static int mix2d(int m00, int m01, int m10, int m11, float x, float y) { + // The weights for each row and column in the matrix + int x1 = ColorU8.normalizedFloatToByte(x), x0 = 255 - x1; + int y1 = ColorU8.normalizedFloatToByte(y), y0 = 255 - y1; + + // Blend across the X-axis + // (M00 * X0) + (M10 * X1) + long row0a = ((((m00 & 0x00FF00FFL) * x0) + (((m10 & 0x00FF00FFL) * x1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL; + long row0b = ((((m00 & 0xFF00FF00L) * x0) + (((m10 & 0xFF00FF00L) * x1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L; + + // (M10 * X0) + (M11 * X1) + long row1a = ((((m01 & 0x00FF00FFL) * x0) + (((m11 & 0x00FF00FFL) * x1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL; + long row1b = ((((m01 & 0xFF00FF00L) * x0) + (((m11 & 0xFF00FF00L) * x1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L; + + // Blend across the Y-axis + // (ROW0 * Y0) + (ROW1 * Y1) + long result = ((((row0a * y0) + ((row1a * y1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) | + ((((row0b * y0) + ((row1b * y1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L); + + return (int) result; } /** - * Multiplies the 32-bit colors with one component + *

Multiplies the packed 8-bit values component-wise to produce 16-bit intermediaries, and then round to the + * nearest 8-bit representation (similar to floating-point.)

+ * + * @param color0 The first color to multiply + * @param color1 The second color to multiply + * @return The product of the two colors + */ + public static int mulComponentWise(int color0, int color1) { + int comp0 = ((((color0 >>> 0) & 0xFF) * ((color1 >>> 0) & 0xFF)) + 0xFF) >>> 8; + int comp1 = ((((color0 >>> 8) & 0xFF) * ((color1 >>> 8) & 0xFF)) + 0xFF) >>> 8; + int comp2 = ((((color0 >>> 16) & 0xFF) * ((color1 >>> 16) & 0xFF)) + 0xFF) >>> 8; + int comp3 = ((((color0 >>> 24) & 0xFF) * ((color1 >>> 24) & 0xFF)) + 0xFF) >>> 8; + + return (comp0 << 0) | (comp1 << 8) | (comp2 << 16) | (comp3 << 24); + } + + /** + *

Multiplies each 8-bit component against the factor to produce 16-bit intermediaries, and then round to the + * nearest 8-bit representation (similar to floating-point.)

+ * + *

The results are undefined if {@param factor} is not within the interval [0, 255].

+ * + * @param color The packed color values + * @param factor The multiplication factor (in 0..255 range) + * @return The result of the multiplication + */ + public static int mul(int color, int factor) { + // De-interleave the 8-bit component lanes into high and low halves. + // Perform 8-bit multiplication to produce Q8.8 fixed-point intermediaries. + final long hi = (color & 0x00FF00FFL) * factor; + final long lo = (color & 0xFF00FF00L) * factor; + + // Round the Q8.8 fixed-point values to the nearest integer, and interleave the high and low halves to + // produce the packed result. + final long result = + (((hi + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) | + (((lo + 0xFF00FF00L) >>> 8) & 0xFF00FF00L); + + return (int) result; + + } + + /** + * See {@link #mul(int, int)}, which this function is identical to, except that it takes a floating point value in + * the interval of [0.0, 1.0] and maps it to [0, 255]. + * + *

The results are undefined if {@param factor} is not within the interval [0.0, 1.0].

*/ - public static int mulSingle(int a, int b) { - // Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers, - // and then shift the high half of each 16-bit integer into 8-bit integers. - int c0 = (((a) & 0xFF) * b) >> 8; - int c1 = (((a >> 8) & 0xFF) * b) >> 8; - int c2 = (((a >> 16) & 0xFF) * b) >> 8; - int c3 = (((a >> 24) & 0xFF) * b) >> 8; - - // Pack the components - return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24); + public static int mul(int color, float factor) { + return mul(color, ColorU8.normalizedFloatToByte(factor)); } } diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java index 142e560113..5ab9042091 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java @@ -7,7 +7,7 @@ /** * 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]. - * + *

* | 32 | 24 | 16 | 8 | * | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 | * | Padding | X | Y | Z | @@ -94,7 +94,7 @@ public static int flipPacked(int norm) { /** * Returns true if the two packed normals are opposite directions. - * + *

* TODO: this could possibly be faster by using normA == (~normB + 0x010101) but * that has to special case when a component is zero since that wouldn't * overflow correctly back to zero. (~0+1 == 0 but not if it's somewhere inside diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java index 0f484805a4..16373c6373 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java @@ -4,7 +4,7 @@ public class ColorAttribute { public static void set(long ptr, int color) { - MemoryUtil.memPutInt(ptr + 0, color); + MemoryUtil.memPutInt(ptr, color); } public static int get(long ptr) { diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java index 4f58ce9436..dc15bb9359 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java @@ -4,7 +4,7 @@ public class NormalAttribute { public static void set(long ptr, int normal) { - MemoryUtil.memPutInt(ptr + 0, normal); + MemoryUtil.memPutInt(ptr, normal); } public static int get(long ptr) { diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java index 9a597597b8..0aafec17ad 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java @@ -4,20 +4,20 @@ public class PositionAttribute { public static void put(long ptr, float x, float y, float z) { - MemoryUtil.memPutFloat(ptr + 0, x); - MemoryUtil.memPutFloat(ptr + 4, y); - MemoryUtil.memPutFloat(ptr + 8, z); + MemoryUtil.memPutFloat(ptr + 0L, x); + MemoryUtil.memPutFloat(ptr + 4L, y); + MemoryUtil.memPutFloat(ptr + 8L, z); } public static float getX(long ptr) { - return MemoryUtil.memGetFloat(ptr + 0); + return MemoryUtil.memGetFloat(ptr + 0L); } public static float getY(long ptr) { - return MemoryUtil.memGetFloat(ptr + 4); + return MemoryUtil.memGetFloat(ptr + 4L); } public static float getZ(long ptr) { - return MemoryUtil.memGetFloat(ptr + 8); + return MemoryUtil.memGetFloat(ptr + 8L); } } diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java index 002a2bd986..b50a500cbe 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java @@ -38,6 +38,24 @@ static VertexBufferWriter tryOf(VertexConsumer consumer) { return null; } + /** + * Converts a {@link VertexConsumer} into a {@link VertexBufferWriter} if possible and only if it can use optimized + * writes with the requested vertex format. + * + * @param consumer The vertex consumer to create a writer for + * @param format The vertex format that will be written + * @return An implementation of {@link VertexBufferWriter}, or null if the vertex consumer does not support optimized + * writes for the requested vertex format + */ + @Nullable + static VertexBufferWriter tryOf(VertexConsumer consumer, VertexFormat format) { + if (consumer instanceof VertexBufferWriter writer && writer.canUseIntrinsics(format)) { + return writer; + } + + return null; + } + private static RuntimeException createUnsupportedVertexConsumerThrowable(VertexConsumer consumer) { var clazz = consumer.getClass(); var name = clazz.getName(); @@ -73,6 +91,16 @@ default boolean canUseIntrinsics() { return true; } + /** + * Checks whether this writer can use optimized writes for vertices with the specified format. + * + * @param format The vertex format that will be written + * @return true if this writer and any nested consumers can use intrinsics for the vertex format + */ + default boolean canUseIntrinsics(VertexFormat format) { + return this.canUseIntrinsics(); + } + /** * Creates a copy of the source data and pushes it into the specified {@param writer}. This is useful for when * you need to use to re-use the source data after the call, and do not want the {@param writer} to modify diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java new file mode 100644 index 0000000000..e42457e2fb --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java @@ -0,0 +1,75 @@ +package net.caffeinemc.mods.sodium.client.compatibility.checks; + +/** + * "Checks" are used to determine whether the environment we are running within is actually reasonable. Most often, + * failing checks will crash the game and prompt the user for intervention. + */ +class BugChecks { + /** + * Some older drivers for Intel Gen7 Graphics on Windows are defective and will never return from + * a call to

glClientWaitSync
. As there is no way to recover the main thread after the + * deadlock occurs, trying to work around this seems to be impossible. Updating the driver to version + * 15.33.53.5161 resolves the problem, and is the currently recommended solution. + * GitHub Issue + */ + public static final boolean ISSUE_899 = configureCheck("issue899", true); + + /** + * Since version 526.47 of the NVIDIA Graphics Driver, the OpenGL user-mode driver will attempt to detect + * Minecraft with hard-coded logic in the driver, and enable broken optimizations (referred to as "Threaded + * Optimizations"). This will cause crashes during framebuffer initialization (and some draw calls) for unclear + * reasons, especially on systems with hybrid graphics. Furthermore, performance is often severely degraded + * due to bubbles in the NVIDIA command submission thread, especially when other mods query context state. + *

+ * Sodium can prevent the detection of Minecraft and the enablement of these unstable optimizations, but the logic + * is extensive and depends heavily on the exact operating system and graphics driver version used. Some older + * graphics driver versions have no workaround available presently. + * GitHub Issue + */ + public static final boolean ISSUE_1486 = configureCheck("issue1486", true); + + /** + * Older versions of the RivaTuner Statistics Server will attempt to use legacy OpenGL functions in a Core + * profile, which causes either a crash or excessive log spam, depending on the OpenGL implementation. This is + * because RivaTuner relies on replacing the OpenGL context immediately after the application initializes it, + * but does so improperly when a No Error Context is used. This problem can't be avoided by simply configuring + * RivaTuner to not inject into Minecraft, as it *always* injects first to modify the context, and *then* disables + * itself. + * GitHub Issue + */ + public static final boolean ISSUE_2048 = configureCheck("issue2048", true); + + /** + * LWJGL does not provide API stability guarantees for other libraries using it to create their own C bindings. + * Because of this, the game will crash due to breaking method/type signature changes very early at startup. We + * should not be using these "internal" classes in LWJGL, but the amount of work involved doing everything ourselves + * is astronomical. When Minecraft ships a version of OpenJDK with the Foreign Function & Memory API, we can replace + * our dependency on LWJGL and remove this check. + * GitHub Issue + */ + public static final boolean ISSUE_2561 = configureCheck("issue2561", true); + + /** + * ASUS's GPU Tweak III does not correctly restore OpenGL context state after rendering its in-game overlay, + * which frequently causes graphical corruption, excessive log file spam, and crashes. These problems are + * actually reproducible without any mods installed, but it seems to be exacerbated with Sodium due to how different + * the rendering pipeline is. This problem can be avoided by configuring the application to not inject into + * Minecraft (or Java applications). + * GitHub Issue + */ + public static final boolean ISSUE_2637 = configureCheck("issue2637", true); + + private static boolean configureCheck(String name, boolean defaultValue) { + var propertyValue = System.getProperty(getPropertyKey(name), null); + + if (propertyValue == null) { + return defaultValue; + } + + return Boolean.parseBoolean(propertyValue); + } + + private static String getPropertyKey(String name) { + return "sodium.checks." + name; + } +} diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java new file mode 100644 index 0000000000..d37572b0f4 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java @@ -0,0 +1,63 @@ +package net.caffeinemc.mods.sodium.client.compatibility.checks; + +import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaDriverVersion; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; +import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; +import net.caffeinemc.mods.sodium.client.platform.PlatformHelper; + +class GraphicsDriverChecks { + static void postContextInit(NativeWindowHandle window, GlContextInfo context) { + var vendor = GraphicsAdapterVendor.fromContext(context); + + if (vendor == GraphicsAdapterVendor.UNKNOWN) { + return; + } + + if (vendor == GraphicsAdapterVendor.INTEL && BugChecks.ISSUE_899) { + var installedVersion = IntelWorkarounds.findIntelDriverMatchingBug899(); + + if (installedVersion != null) { + var installedVersionString = installedVersion.toString(); + + PlatformHelper.showCriticalErrorAndClose(window, + "Sodium Renderer - Unsupported Driver", + """ + The game failed to start because the currently installed Intel Graphics Driver is not \ + compatible. + + Installed version: ###CURRENT_DRIVER### + Required version: 10.18.10.5161 (or newer) + + Please click the 'Help' button to read more about how to fix this problem.""" + .replace("###CURRENT_DRIVER###", installedVersionString), + "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/intel/gh-899"); + } + } + + if (vendor == GraphicsAdapterVendor.NVIDIA && BugChecks.ISSUE_1486) { + var installedVersion = NvidiaWorkarounds.findNvidiaDriverMatchingBug1486(); + + if (installedVersion != null) { + var installedVersionString = NvidiaDriverVersion.parse(installedVersion) + .toString(); + + PlatformHelper.showCriticalErrorAndClose(window, + "Sodium Renderer - Unsupported Driver", + """ + The game failed to start because the currently installed NVIDIA Graphics Driver is not \ + compatible. + + Installed version: ###CURRENT_DRIVER### + Required version: 536.23 (or newer) + + Please click the 'Help' button to read more about how to fix this problem.""" + .replace("###CURRENT_DRIVER###", installedVersionString), + "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/nvidia/gh-1486"); + + } + } + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java similarity index 93% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java index 8107366ef7..8af1a8d70f 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java @@ -105,10 +105,10 @@ You appear to be using an older version of RivaTuner Statistics Server (RTSS) wh You must either update to a newer version (7.3.4 and later) or close the RivaTuner Statistics Server application. For more information on how to solve this problem, click the 'Help' button.""", - "https://github.com/CaffeineMC/sodium/wiki/Known-Issues#rtss-incompatible"); + "https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"); throw new RuntimeException("The installed version of RivaTuner Statistics Server (RTSS) is not compatible with Sodium, " + - "see here for more details: https://github.com/CaffeineMC/sodium/wiki/Known-Issues#rtss-incompatible"); + "see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048"); } } @@ -133,10 +133,10 @@ private static void checkASUSGpuTweakIII(NativeWindowHandle window) { b) Completely uninstall the ASUS GPU Tweak III application. For more information on how to solve this problem, click the 'Help' button.""", - "https://github.com/CaffeineMC/sodium/wiki/Known-Issues#asus-gtiii-incompatible"); + "https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"); throw new RuntimeException("ASUS GPU Tweak III is not compatible with Minecraft, " + - "see here for more details: https://github.com/CaffeineMC/sodium/wiki/Known-Issues#asus-gtiii-incompatible"); + "see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637"); } private static @Nullable WindowsFileVersion findRTSSModuleVersion() { diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java similarity index 74% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java index 85f5d92488..79be746ff2 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java @@ -1,7 +1,8 @@ package net.caffeinemc.mods.sodium.client.compatibility.checks; -import net.caffeinemc.mods.sodium.client.console.Console; -import net.caffeinemc.mods.sodium.client.console.message.MessageLevel; +import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; +import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,13 +13,14 @@ public class PostLaunchChecks { private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-PostlaunchChecks"); - public static void onContextInitialized() { + public static void onContextInitialized(NativeWindowHandle window, GlContextInfo context) { + GraphicsDriverChecks.postContextInit(window, context); + NvidiaWorkarounds.applyContextChanges(context); + // FIXME: This can be determined earlier, but we can't access the GUI classes in pre-launch if (isUsingPojavLauncher()) { - Console.instance().logMessage(MessageLevel.SEVERE, "sodium.console.pojav_launcher", true, 30.0); - LOGGER.error("It appears that PojavLauncher is being used with an OpenGL compatibility layer. This will " + - "likely cause severe performance issues, graphical issues, and crashes when used with Sodium. This " + - "configuration is not supported -- you are on your own!"); + throw new RuntimeException("It appears that you are using PojavLauncher, which is not supported when " + + "using Sodium. Please check your mods list."); } } diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java new file mode 100644 index 0000000000..044c49fb64 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java @@ -0,0 +1,162 @@ +package net.caffeinemc.mods.sodium.client.compatibility.checks; + +import net.caffeinemc.mods.sodium.client.platform.PlatformHelper; +import org.lwjgl.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; + +/** + * Performs OpenGL driver validation before the game creates an OpenGL context. This runs during the earliest possible + * opportunity at game startup, and uses a custom hardware prober to search for problematic drivers. + */ +public class PreLaunchChecks { + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-PreLaunchChecks"); + + // These version constants are inlined at compile time. + private static final String REQUIRED_LWJGL_VERSION = + Version.VERSION_MAJOR + "." + Version.VERSION_MINOR + "." + Version.VERSION_REVISION; + + public static void checkEnvironment() { + if (BugChecks.ISSUE_2561) { + checkLwjglRuntimeVersion(); + } + } + + private static void checkLwjglRuntimeVersion() { + if (isUsingKnownCompatibleLwjglVersion()) { + return; + } + + + String launcher = getLauncherBrand(); + String codeSource = getLwjglCodeSource(); + String codeSourceFilename = null; + if (codeSource != null) { + String[] components = codeSource.split("/"); + codeSourceFilename = components[components.length-1]; + } + + if (codeSource != null) { + LOGGER.info("Problematic LWJGL version source: {}", codeSource); + } + + boolean isCustomLauncher = !launcher.equals("minecraft-launcher") && !launcher.equals("unknown"); + boolean isLikelyCausedByLauncher = false; + String isLikelyCausedByMod = null; + + if (isCustomLauncher) { + if (codeSourceFilename != null && (codeSourceFilename.startsWith("lwjgl-") || codeSource.contains("/lwjgl/"))) { + isLikelyCausedByLauncher = true; + } + } + if (!isLikelyCausedByLauncher) { + if (codeSource != null && codeSource.endsWith("/mods/"+codeSourceFilename)) { + isLikelyCausedByMod = codeSourceFilename; + } + } + + String advice; + if (isLikelyCausedByMod != null) { + advice = """ + This issue seems to be caused by ###MOD###. + + Removing ###MOD### from your mods folder may fix this issue.""" + .replace("###MOD###", isLikelyCausedByMod); + } else if (launcher.equalsIgnoreCase("prismlauncher")) { + advice = """ + It appears you are using Prism Launcher to start the game. You can \ + likely fix this problem by opening your instance settings and navigating to the Version \ + section in the sidebar."""; + } else if (isLikelyCausedByLauncher) { + advice = """ + You seem to be using ###LAUNCHER###. This issue is likely caused by ###LAUNCHER###. + + You must change the LWJGL version in your launcher to continue. \ + This is usually controlled by the settings for a profile or instance in your launcher. + + If you need assistance fixing the LWJGL version, you should contact ###LAUNCHER###, not Sodium.""" + .replace("###LAUNCHER###", launcher); + } else if (isCustomLauncher) { + advice = """ + You seem to be using ###LAUNCHER###. + + You must change the LWJGL version in your launcher to continue. \ + This is usually controlled by the settings for a profile or instance in your launcher.""" + .replace("###LAUNCHER###", launcher); + } else { + advice = """ + You must change the LWJGL version in your launcher to continue. \ + This is usually controlled by the settings for a profile or instance in your launcher."""; + } + + String message = """ + The game failed to start because the currently active LWJGL version is not \ + compatible. + + Installed version: ###CURRENT_VERSION### + Required version: ###REQUIRED_VERSION### + + ###ADVICE_STRING###""" + .replace("###CURRENT_VERSION###", Version.getVersion()) + .replace("###REQUIRED_VERSION###", REQUIRED_LWJGL_VERSION) + .replace("###ADVICE_STRING###", advice); + + PlatformHelper.showCriticalErrorAndClose(null, "Sodium Renderer - Unsupported LWJGL", message, + "https://link.caffeinemc.net/help/sodium/runtime-issue/lwjgl3/gh-2561"); + } + + private static String getLwjglCodeSource() { + try { + ProtectionDomain domain = Version.class.getProtectionDomain(); + if (domain != null) { + CodeSource source = domain.getCodeSource(); + if (source != null) { + URL location = source.getLocation(); + if (location != null) { + String path = location.getPath(); + if (path != null) { + path = path.replace('\\', '/'); + path = path.split("!")[0]; + return path; + } + } + } + } + } catch (Throwable t) { + LOGGER.error("Error while checking code source of LWJGL", t); + } + return null; + } + + private static boolean isUsingKnownCompatibleLwjglVersion() { + return Version.getVersion() + .startsWith(REQUIRED_LWJGL_VERSION); + } + + private static String getLauncherBrand() { + String brand = System.getProperty("minecraft.launcher.brand", "unknown"); + if (brand.equals("unknown")) { + // Lunar Client uses a custom set of launch arguments + // which don't include minecraft.launcher.brand + if (isClassLoaded("com.moonsworth.lunar.genesis.Genesis")) { + return "Lunar Client"; + } else if (System.getProperty("lunar.webosr.url") != null) { + return "Lunar Client"; + } + } + return brand; + } + + private static boolean isClassLoaded(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java new file mode 100644 index 0000000000..f4aa02f263 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.compatibility.environment; + +import org.lwjgl.opengl.GL11C; + +import java.util.Objects; + +public record GlContextInfo(String vendor, String renderer, String version) { + public static GlContextInfo create() { + String vendor = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_VENDOR), + "GL_VENDOR is NULL"); + String renderer = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_RENDERER), + "GL_RENDERER is NULL"); + String version = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_VERSION), + "GL_VERSION is NULL"); + + return new GlContextInfo(vendor, renderer, version); + } +} diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java new file mode 100644 index 0000000000..c232c0faa9 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java @@ -0,0 +1,36 @@ +package net.caffeinemc.mods.sodium.client.compatibility.environment; + +import java.util.Locale; + +public class OsUtils { + private static final OperatingSystem OS = determineOs(); + + public static OperatingSystem determineOs() { + var name = System.getProperty("os.name"); + + if (name != null) { + var normalized = name.toLowerCase(Locale.ROOT); + + if (normalized.startsWith("windows")) { + return OperatingSystem.WIN; + } else if (normalized.startsWith("mac")) { + return OperatingSystem.MAC; + } else if (normalized.startsWith("linux")) { + return OperatingSystem.LINUX; + } + } + + return OperatingSystem.UNKNOWN; + } + + public static OperatingSystem getOs() { + return OS; + } + + public enum OperatingSystem { + WIN, + MAC, + LINUX, + UNKNOWN + } +} \ No newline at end of file diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java similarity index 99% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java index 4bfb59179a..64a251bb54 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java @@ -2,7 +2,6 @@ import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java new file mode 100644 index 0000000000..f18f4c4cb9 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java @@ -0,0 +1,77 @@ +package net.caffeinemc.mods.sodium.client.compatibility.environment.probe; + +import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +public enum GraphicsAdapterVendor { + NVIDIA, + AMD, + INTEL, + UNKNOWN; + + // Intel Gen 4, 5, 6 - ig4icd + // Intel Gen 7 - ig7icd + // Intel Gen 7.5 - ig75icd + // Intel Gen 8 - ig8icd + // Intel Gen 9, 9.5 - ig9icd + // Intel Gen 11 - ig11icd + // Intel Xe-LP - ig12icd (early drivers) or igxelpicd (later drivers) + // Intel Xe-HP - igxehpicd + // Intel Xe-HPG - igxehpgicd + // Intel Xe2-LPG - igxe2lpgicd + // Intel Xe2-HPG - igxe2hpgicd + private static final Pattern INTEL_ICD_PATTERN = + Pattern.compile("ig(4|7|75|8|9|11|12|(xe2?(hpg?|lpg?)))icd(32|64)\\.dll", Pattern.CASE_INSENSITIVE); + + private static final Pattern NVIDIA_ICD_PATTERN = + Pattern.compile("nvoglv(32|64)\\.dll", Pattern.CASE_INSENSITIVE); + + private static final Pattern AMD_ICD_PATTERN = + Pattern.compile("(atiglpxx|atig6pxx)\\.dll", Pattern.CASE_INSENSITIVE); + + @NotNull + static GraphicsAdapterVendor fromPciVendorId(String vendor) { + if (vendor.contains("0x1002")) { + return AMD; + } else if (vendor.contains("0x10de")) { + return NVIDIA; + } else if (vendor.contains("0x8086")) { + return INTEL; + } + + return UNKNOWN; + } + + @NotNull + public static GraphicsAdapterVendor fromIcdName(String name) { + if (matchesPattern(INTEL_ICD_PATTERN, name)) { + return INTEL; + } else if (matchesPattern(NVIDIA_ICD_PATTERN, name)) { + return NVIDIA; + } else if (matchesPattern(AMD_ICD_PATTERN, name)) { + return AMD; + } else { + return UNKNOWN; + } + } + + @NotNull + public static GraphicsAdapterVendor fromContext(GlContextInfo context) { + var vendor = context.vendor(); + + return switch (vendor) { + case "NVIDIA Corporation" -> NVIDIA; + case "Intel", "Intel Open Source Technology Center" -> INTEL; + case "AMD", "ATI Technologies Inc." -> AMD; + default -> UNKNOWN; + }; + + } + + private static boolean matchesPattern(Pattern pattern, String name) { + return pattern.matcher(name) + .matches(); + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java similarity index 57% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java index 1e6afc0cad..57571c3f14 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java @@ -1,15 +1,15 @@ package net.caffeinemc.mods.sodium.client.compatibility.workarounds; import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterInfo; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor; -import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT; -import org.jetbrains.annotations.Nullable; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd.AmdWorkarounds; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -37,59 +37,26 @@ private static Set findNecessaryWorkarounds() { var workarounds = EnumSet.noneOf(Reference.class); var operatingSystem = OsUtils.getOs(); - var graphicsAdapters = GraphicsAdapterProbe.getAdapters(); + if (NvidiaWorkarounds.isNvidiaGraphicsCardPresent()) { + workarounds.add(Reference.NVIDIA_THREADED_OPTIMIZATIONS_BROKEN); + } - if (isUsingNvidiaGraphicsCard(operatingSystem, graphicsAdapters)) { - workarounds.add(Reference.NVIDIA_THREADED_OPTIMIZATIONS); + if (AmdWorkarounds.isAmdGraphicsCardPresent() && operatingSystem == OsUtils.OperatingSystem.WIN) { + workarounds.add(Reference.AMD_GAME_OPTIMIZATION_BROKEN); } - if (isUsingIntelGen8OrOlder()) { + if (IntelWorkarounds.isUsingIntelGen8OrOlder()) { workarounds.add(Reference.INTEL_FRAMEBUFFER_BLIT_CRASH_WHEN_UNFOCUSED); workarounds.add(Reference.INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE); } if (operatingSystem == OsUtils.OperatingSystem.LINUX) { - var session = System.getenv("XDG_SESSION_TYPE"); - - if (session == null) { - LOGGER.warn("Unable to determine desktop session type because the environment variable XDG_SESSION_TYPE " + - "is not set! Your user session may not be configured correctly."); - } - - if (Objects.equals(session, "wayland")) { - // This will also apply under Xwayland, even though the problem does not happen there - workarounds.add(Reference.NO_ERROR_CONTEXT_UNSUPPORTED); - } + workarounds.add(Reference.NO_ERROR_CONTEXT_UNSUPPORTED); } return Collections.unmodifiableSet(workarounds); } - private static boolean isUsingIntelGen8OrOlder() { - if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) { - return false; - } - - for (var adapter : GraphicsAdapterProbe.getAdapters()) { - if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { - @Nullable var driverName = wddmAdapterInfo.getOpenGlIcdName(); - - // Intel OpenGL ICD for legacy GPUs - if (driverName != null && driverName.matches("ig(7|75|8)icd(32|64)")) { - return true; - } - } - } - - return false; - } - - private static boolean isUsingNvidiaGraphicsCard(OsUtils.OperatingSystem operatingSystem, Collection adapters) { - - return (operatingSystem == OsUtils.OperatingSystem.WIN || operatingSystem == OsUtils.OperatingSystem.LINUX) && - adapters.stream().anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.NVIDIA); - } - public static boolean isWorkaroundEnabled(Reference id) { return ACTIVE_WORKAROUNDS.get() .contains(id); @@ -101,10 +68,11 @@ public enum Reference { * performance issues and crashes. * GitHub Issue */ - NVIDIA_THREADED_OPTIMIZATIONS, + NVIDIA_THREADED_OPTIMIZATIONS_BROKEN, /** - * Requesting a No Error Context causes a crash at startup when using a Wayland session. + * Requesting a No Error Context causes a crash at startup when using a Wayland session on GLFW + <3.4. * GitHub Issue */ NO_ERROR_CONTEXT_UNSUPPORTED, @@ -113,6 +81,7 @@ public enum Reference { * Intel's graphics driver for Gen8 and older seems to be faulty and causes a crash when calling * glFramebufferBlit after the window loses focus. * GitHub Issue + * GitHub Issue */ INTEL_FRAMEBUFFER_BLIT_CRASH_WHEN_UNFOCUSED, @@ -122,6 +91,15 @@ public enum Reference { * the base model. * GitHub Issue */ - INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE + INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE, + + /** + * AMD's graphics driver starting at 25.10.2 does not correctly handle glMapBufferRange + * when minecraft is detected, causing terrain rendering to go invisible if the launcher allows minecraft + * to be detected. Most commonly this happens with some third party PVP clients, + * but can happen with other launchers. + * GitHub Issue + */ + AMD_GAME_OPTIMIZATION_BROKEN } } diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java new file mode 100644 index 0000000000..9b6ddbc396 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java @@ -0,0 +1,74 @@ +package net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd; + +import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; +import net.caffeinemc.mods.sodium.client.platform.windows.WindowsCommandLine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AmdWorkarounds { + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-AmdWorkarounds"); + + public static boolean isAmdGraphicsCardPresent() { + return GraphicsAdapterProbe.getAdapters() + .stream() + .anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.AMD); + } + + public static void undoEnvironmentChanges() { + if (OsUtils.getOs() == OsUtils.OperatingSystem.WIN) { + undoEnvironmentChanges$Windows(); + } + } + + private static void undoEnvironmentChanges$Windows() { + WindowsCommandLine.resetCommandLine(); + } + + public static void applyEnvironmentChanges() { + // We can't know if the OpenGL context will actually be initialized using the AMD ICD, but we need to + // modify the process environment *now* otherwise the driver will initialize with bad settings. For non-AMD + // drivers, these workarounds are not likely to cause issues. + if (!isAmdGraphicsCardPresent()) { + return; + } + + // Skip applying the AMD workaround if the user also has an Nvidia gpu, to avoid attempting to overwrite the + // process command line twice. + if (NvidiaWorkarounds.isNvidiaGraphicsCardPresent()) { + return; + } + + try { + if (OsUtils.getOs() == OsUtils.OperatingSystem.WIN) { + LOGGER.info("Modifying process environment to apply workarounds for the AMD graphics driver..."); + + applyEnvironmentChanges$Windows(); + } + } catch (Throwable t) { + LOGGER.error("Failed to modify the process environment", t); + logWarning(); + } + } + + + private static void applyEnvironmentChanges$Windows() { + // The new AMD drivers rely on parsing the command line arguments to detect Minecraft. + // When they do they apply an optimization that is broken with sodium present + // This stops AMD drivers from detecting the game + WindowsCommandLine.setCommandLine("net.caffeinemc.sodium / net.minecraft.client.main.Main /"); + } + + private static void logWarning() { + LOGGER.error("READ ME!"); + LOGGER.error("READ ME! The workarounds for the AMD Graphics Driver did not apply correctly!"); + LOGGER.error("READ ME! You may run into unexplained graphical issues."); + LOGGER.error("READ ME! More information about what went wrong can be found above this message."); + LOGGER.error("READ ME!"); + LOGGER.error("READ ME! Please help us understand why this problem occurred by opening a bug report on our issue tracker:"); + LOGGER.error("READ ME! https://github.com/CaffeineMC/sodium/issues"); + LOGGER.error("READ ME!"); + } +} diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java new file mode 100644 index 0000000000..94058d1ded --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java @@ -0,0 +1,59 @@ +package net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel; + +import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; +import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion; +import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT; +import org.jetbrains.annotations.Nullable; + +public class IntelWorkarounds { + // https://github.com/CaffeineMC/sodium/issues/899 + public static @Nullable WindowsFileVersion findIntelDriverMatchingBug899() { + if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) { + return null; + } + + for (var adapter : GraphicsAdapterProbe.getAdapters()) { + if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { + String driverName = wddmAdapterInfo.getOpenGlIcdName(); + + if (driverName == null) { + continue; + } + + var driverVersion = wddmAdapterInfo.openglIcdVersion(); + + // Intel OpenGL ICD for Generation 7 GPUs + if (driverName.matches("ig7icd(32|64).dll")) { + // https://www.intel.com/content/www/us/en/support/articles/000005654/graphics.html + // Anything which matches the 15.33 driver scheme (WDDM x.y.10.w) should be checked + // Drivers before build 5161 are assumed to have bugs with synchronization primitives + if (driverVersion.z() == 10 && driverVersion.w() < 5161) { + return driverVersion; + } + } + } + } + + return null; + } + + public static boolean isUsingIntelGen8OrOlder() { + if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) { + return false; + } + + for (var adapter : GraphicsAdapterProbe.getAdapters()) { + if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { + var driverName = wddmAdapterInfo.getOpenGlIcdName(); + + // Intel OpenGL ICD for legacy GPUs + if (driverName != null && driverName.matches("ig(7|75|8)icd(32|64)\\.dll")) { + return true; + } + } + } + + return false; + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java new file mode 100644 index 0000000000..572dc8b088 --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java @@ -0,0 +1,152 @@ +package net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia; + +import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo; +import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; +import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils.OperatingSystem; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.platform.unix.Libc; +import net.caffeinemc.mods.sodium.client.platform.windows.WindowsCommandLine; +import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion; +import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL32C; +import org.lwjgl.opengl.KHRDebug; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NvidiaWorkarounds { + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-NvidiaWorkarounds"); + + public static boolean isNvidiaGraphicsCardPresent() { + return GraphicsAdapterProbe.getAdapters() + .stream() + .anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.NVIDIA); + } + + // https://github.com/CaffeineMC/sodium/issues/1486 + // The way which NVIDIA tries to detect the Minecraft process could not be circumvented until fairly recently + // So we require that an up-to-date graphics driver is installed so that our workarounds can disable the Threaded + // Optimizations driver hack. + public static @Nullable WindowsFileVersion findNvidiaDriverMatchingBug1486() { + // The Linux driver has two separate branches which have overlapping version numbers, despite also having + // different feature sets. As a result, we can't reliably determine which Linux drivers are broken... + if (OsUtils.getOs() != OperatingSystem.WIN) { + return null; + } + + for (var adapter : GraphicsAdapterProbe.getAdapters()) { + if (adapter.vendor() != GraphicsAdapterVendor.NVIDIA) { + continue; + } + + if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { + var driverVersion = wddmAdapterInfo.openglIcdVersion(); + + if (driverVersion.z() == 15) { // Only match 5XX.XX drivers + // Broken in x.y.15.2647 (526.47) + // Fixed in x.y.15.3623 (536.23) + if (driverVersion.w() >= 2647 && driverVersion.w() < 3623) { + return driverVersion; + } + } + } + } + + return null; + } + + public static void applyEnvironmentChanges() { + // We can't know if the OpenGL context will actually be initialized using the NVIDIA ICD, but we need to + // modify the process environment *now* otherwise the driver will initialize with bad settings. For non-NVIDIA + // drivers, these workarounds are not likely to cause issues. + if (!isNvidiaGraphicsCardPresent()) { + return; + } + + LOGGER.info("Modifying process environment to apply workarounds for the NVIDIA graphics driver..."); + + try { + if (OsUtils.getOs() == OperatingSystem.WIN) { + applyEnvironmentChanges$Windows(); + } else if (OsUtils.getOs() == OperatingSystem.LINUX) { + applyEnvironmentChanges$Linux(); + } + } catch (Throwable t) { + LOGGER.error("Failed to modify the process environment", t); + logWarning(); + } + } + + + private static void applyEnvironmentChanges$Windows() { + // The NVIDIA drivers rely on parsing the command line arguments to detect Minecraft. We need to + // make sure that it detects the game so that *some* important optimizations are applied. Later, + // we will try to enable GL_DEBUG_OUTPUT_SYNCHRONOUS so that "Threaded Optimizations" cannot + // be enabled. + WindowsCommandLine.setCommandLine("net.caffeinemc.sodium / net.minecraft.client.main.Main /"); + } + + private static void applyEnvironmentChanges$Linux() { + // Unlike Windows, we can just request that it not use threaded optimizations instead. + Libc.setEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", "0"); + } + + public static void undoEnvironmentChanges() { + if (OsUtils.getOs() == OperatingSystem.WIN) { + undoEnvironmentChanges$Windows(); + } + } + + private static void undoEnvironmentChanges$Windows() { + WindowsCommandLine.resetCommandLine(); + } + + public static void applyContextChanges(GlContextInfo context) { + // The context may not have been initialized with the NVIDIA ICD, even if we think there is an NVIDIA + // graphics adapter in use. Because enabling these workarounds have the potential to severely hurt performance + // on other drivers, make sure we exit now. + if (GraphicsAdapterVendor.fromContext(context) != GraphicsAdapterVendor.NVIDIA) { + return; + } + + LOGGER.info("Modifying OpenGL context to apply workarounds for the NVIDIA graphics driver..."); + + if (Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_THREADED_OPTIMIZATIONS_BROKEN)) { + if (OsUtils.getOs() == OperatingSystem.WIN) { + applyContextChanges$Windows(); + } + } + } + + private static void applyContextChanges$Windows() { + // On Windows, the NVIDIA drivers do not have any environment variable to control whether + // "Threaded Optimizations" are enabled. But we can enable the "GL_DEBUG_OUTPUT_SYNCHRONOUS" option to + // achieve the same effect. + var capabilities = GL.getCapabilities(); + + if (capabilities.GL_KHR_debug) { + LOGGER.info("Enabling GL_DEBUG_OUTPUT_SYNCHRONOUS to force the NVIDIA driver to disable threaded " + + "command submission"); + GL32C.glEnable(KHRDebug.GL_DEBUG_OUTPUT_SYNCHRONOUS); + } else { + LOGGER.error("GL_KHR_debug does not appear to be supported, unable to disable threaded " + + "command submission!"); + logWarning(); + } + } + + private static void logWarning() { + LOGGER.error("READ ME!"); + LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!"); + LOGGER.error("READ ME! You are very likely going to run into unexplained crashes and severe performance issues."); + LOGGER.error("READ ME! More information about what went wrong can be found above this message."); + LOGGER.error("READ ME!"); + LOGGER.error("READ ME! Please help us understand why this problem occurred by opening a bug report on our issue tracker:"); + LOGGER.error("READ ME! https://github.com/CaffeineMC/sodium/issues"); + LOGGER.error("READ ME!"); + + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/Console.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/Console.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/Console.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/Console.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/Message.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/Message.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/Message.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/Message.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java similarity index 92% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java index bf0cfb47cb..7aa49791f3 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.platform; import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; +import net.caffeinemc.mods.sodium.client.platform.windows.api.Shell32; import net.caffeinemc.mods.sodium.client.platform.windows.api.User32; import net.caffeinemc.mods.sodium.client.platform.windows.api.msgbox.MsgBoxCallback; import net.caffeinemc.mods.sodium.client.platform.windows.api.msgbox.MsgBoxParamSw; @@ -8,9 +9,6 @@ import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; -import java.awt.*; -import java.io.IOException; -import java.net.URI; import java.nio.ByteBuffer; import java.util.Objects; @@ -58,11 +56,7 @@ public void showMessageBox(NativeWindowHandle window, if (helpUrl != null) { msgBoxCallback = MsgBoxCallback.create(lpHelpInfo -> { - try { - Desktop.getDesktop().browse(URI.create(helpUrl)); - } catch (IOException e) { - System.out.println("Failed to open! Giving up."); - } + Shell32.browseUrl(window, helpUrl); }); } else { msgBoxCallback = null; diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java new file mode 100644 index 0000000000..dfc1f2b77f --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java @@ -0,0 +1,29 @@ +package net.caffeinemc.mods.sodium.client.platform; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PlatformHelper { + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-EarlyDriverScanner"); + + public static void showCriticalErrorAndClose( + @Nullable NativeWindowHandle window, + @NotNull String messageTitle, + @NotNull String messageBody, + @NotNull String helpUrl) + { + // Always print the information to the log file first, just in case we can't show the message box. + LOGGER.error(""" + ###ERROR_DESCRIPTION### + + For more information, please see: ###HELP_URL###""" + .replace("###ERROR_DESCRIPTION###", messageBody) + .replace("###HELP_URL###", helpUrl)); + + // Try to show a graphical message box (if the platform supports it) and shut down the game. + MessageBox.showMessageBox(window, MessageBox.IconType.ERROR, messageTitle, messageBody, helpUrl); + System.exit(1 /* failure code */); + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java similarity index 75% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java index 3165ad6d1e..721bb63263 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32; import org.lwjgl.system.MemoryUtil; + import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Objects; @@ -17,11 +18,15 @@ public static void setCommandLine(String modifiedCmdline) { // Pointer into the command-line arguments stored within the Windows process structure // We do not own this memory, and it should not be freed. var pCmdline = Kernel32.getCommandLine(); + var pCmdlineA = Kernel32.getCommandLineA(); // The original command-line the process was started with. var cmdline = MemoryUtil.memUTF16(pCmdline); var cmdlineLen = MemoryUtil.memLengthUTF16(cmdline, true); + var cmdlineA = MemoryUtil.memASCII(pCmdlineA); + var cmdLineLenA = MemoryUtil.memLengthASCII(cmdlineA, true); + if (MemoryUtil.memLengthUTF16(modifiedCmdline, true) > cmdlineLen) { // We can never write a string which is larger than what we were given, as there // may not be enough space remaining. Realistically, this should never happen, since @@ -30,12 +35,18 @@ public static void setCommandLine(String modifiedCmdline) { throw new BufferOverflowException(); } + if (MemoryUtil.memLengthASCII(modifiedCmdline, true) > cmdLineLenA) { + throw new BufferOverflowException(); + } + ByteBuffer buffer = MemoryUtil.memByteBuffer(pCmdline, cmdlineLen); + ByteBuffer bufferA = MemoryUtil.memByteBuffer(pCmdlineA, cmdLineLenA); // Write the new command line arguments into the process structure. // The Windows API documentation explicitly says this is forbidden, but it *does* give us a pointer // directly into the PEB structure, so... MemoryUtil.memUTF16(modifiedCmdline, true, buffer); + MemoryUtil.memASCII(modifiedCmdline, true, bufferA); // Make sure we can actually see our changes in the process structure // We don't know if this could ever actually happen, but since we're doing something pretty hacky @@ -43,8 +54,11 @@ public static void setCommandLine(String modifiedCmdline) { if (!Objects.equals(modifiedCmdline, MemoryUtil.memUTF16(pCmdline))) { throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change"); } + if (!Objects.equals(modifiedCmdline, MemoryUtil.memASCII(pCmdlineA))) { + throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change"); + } - ACTIVE_COMMAND_LINE_HOOK = new CommandLineHook(cmdline, buffer); + ACTIVE_COMMAND_LINE_HOOK = new CommandLineHook(cmdline, cmdlineA, buffer, bufferA); } public static void resetCommandLine() { @@ -56,13 +70,17 @@ public static void resetCommandLine() { private static class CommandLineHook { private final String cmdline; + private final String cmdlineA; private final ByteBuffer cmdlineBuf; + private final ByteBuffer cmdlineBufA; private boolean active = true; - private CommandLineHook(String cmdline, ByteBuffer cmdlineBuf) { + private CommandLineHook(String cmdline, String cmdlineA, ByteBuffer cmdlineBuf, ByteBuffer cmdlineBufA) { this.cmdline = cmdline; + this.cmdlineA = cmdlineA; this.cmdlineBuf = cmdlineBuf; + this.cmdlineBufA = cmdlineBufA; } public void uninstall() { @@ -73,6 +91,7 @@ public void uninstall() { // Restore the original value of the command line arguments // Must be null-terminated (as it was given to us) MemoryUtil.memUTF16(this.cmdline, true, this.cmdlineBuf); + MemoryUtil.memASCII(this.cmdlineA, true, this.cmdlineBufA); this.active = false; } diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java similarity index 80% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java index 3341963a38..680c071807 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.client.platform.windows.api; -import org.apache.commons.lang3.Validate; import org.lwjgl.system.JNI; import org.lwjgl.system.SharedLibrary; @@ -21,19 +20,24 @@ public static boolean isD3DKMTSupported() { // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtqueryadapterinfo public static int /* NTSTATUS */ nd3dKmtQueryAdapterInfo(long ptr /* D3DKMT_QUERYADAPTERINFO */) { - Validate.isTrue(PFN_D3DKMTQueryAdapterInfo != NULL); - return JNI.callPI(ptr, PFN_D3DKMTQueryAdapterInfo); + return JNI.callPI(ptr, checkPfn(PFN_D3DKMTQueryAdapterInfo)); } // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtenumadapters2 public static int /* NTSTATUS */ nD3DKMTEnumAdapters(long ptr /* D3DKMT_ENUMADAPTERS */) { - Validate.isTrue(PFN_D3DKMTEnumAdapters != NULL); - return JNI.callPI(ptr, PFN_D3DKMTEnumAdapters); + return JNI.callPI(ptr, checkPfn(PFN_D3DKMTEnumAdapters)); } // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtcloseadapter public static int /* NTSTATUS */ nD3DKMTCloseAdapter(long ptr /* D3DKMT_CLOSEADAPTER */) { - Validate.isTrue(PFN_D3DKMTCloseAdapter != NULL); - return JNI.callPI(ptr, PFN_D3DKMTCloseAdapter); + return JNI.callPI(ptr, checkPfn(PFN_D3DKMTCloseAdapter)); + } + + private static long checkPfn(long pfn) { + if (pfn == NULL) { + throw new NullPointerException("Function pointer not available"); + } + + return pfn; } } diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java similarity index 94% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java index 4b1c62ae3a..f8b6fef62a 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.Nullable; import org.lwjgl.PointerBuffer; import org.lwjgl.system.*; + import java.nio.ByteBuffer; public class Kernel32 { @@ -14,6 +15,7 @@ public class Kernel32 { private static final int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 1 << 2; private static final long PFN_GetCommandLineW; + private static final long PFN_GetCommandLineA; private static final long PFN_SetEnvironmentVariableW; private static final long PFN_GetModuleHandleExW; @@ -24,6 +26,7 @@ public class Kernel32 { static { PFN_GetCommandLineW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineW"); + PFN_GetCommandLineA = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineA"); PFN_SetEnvironmentVariableW = APIUtil.apiGetFunctionAddress(LIBRARY, "SetEnvironmentVariableW"); PFN_GetModuleHandleExW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetModuleHandleExW"); PFN_GetLastError = APIUtil.apiGetFunctionAddress(LIBRARY, "GetLastError"); @@ -50,6 +53,10 @@ public static long getCommandLine() { return JNI.callP(PFN_GetCommandLineW); } + public static long getCommandLineA() { + return JNI.callP(PFN_GetCommandLineA); + } + public static long getModuleHandleByNames(String[] names) { for (String name : names) { var handle = getModuleHandleByName(name); diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java new file mode 100644 index 0000000000..5409b7fcea --- /dev/null +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java @@ -0,0 +1,57 @@ +package net.caffeinemc.mods.sodium.client.platform.windows.api; + +import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.JNI; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.SharedLibrary; + +import java.util.Objects; + +import static org.lwjgl.system.APIUtil.apiCreateLibrary; +import static org.lwjgl.system.APIUtil.apiGetFunctionAddressOptional; +import static org.lwjgl.system.MemoryUtil.NULL; + +public class Shell32 { + private static final SharedLibrary LIBRARY = apiCreateLibrary("shell32"); + + private static final long PFN_ShellExecuteW = apiGetFunctionAddressOptional(LIBRARY, "ShellExecuteW"); + + public static void browseUrl(@Nullable NativeWindowHandle window, String url) { + Objects.requireNonNull(url, "URL parameter must be non-null"); + + try (var stack = MemoryStack.stackPush()) { + stack.nUTF16("open", true); + var lpOperation = stack.getPointerAddress(); + + stack.nUTF16(url, true); + var lpFile = stack.getPointerAddress(); + + nShellExecuteW(window != null ? window.getWin32Handle() : NULL, + lpOperation, + lpFile, + NULL, + NULL, + 0x1 /* SW_NORMAL */); + } + } + + public static long nShellExecuteW( + /* HWND */ long hwnd, + /* LPCWSTR */ long lpOperation, + /* LPCWSTR */ long lpFile, + /* LPCWSTR */ long lpParameters, + /* LPCWSTR */ long lpDirectory, + /* INT */ int nShowCmd + ) { + return JNI.invokePPPPPP(hwnd, lpOperation, lpFile, lpParameters, lpDirectory, nShowCmd, checkPfn(PFN_ShellExecuteW)); + } + + private static long checkPfn(long pfn) { + if (pfn == NULL) { + throw new NullPointerException("Function pointer not available"); + } + + return pfn; + } +} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java similarity index 91% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java index 102fe183cf..28cb79b0ac 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java @@ -5,7 +5,6 @@ import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion; import net.caffeinemc.mods.sodium.client.platform.windows.api.Gdi32; import net.caffeinemc.mods.sodium.client.platform.windows.api.version.Version; -import org.apache.commons.io.FilenameUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.system.MemoryStack; @@ -13,7 +12,7 @@ import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; -import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -47,7 +46,7 @@ public static List findGraphicsAdapters() { } } - private static @NotNull ArrayList queryAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapterInfoBuffer) { + private static @NotNull ArrayList queryAdapters(D3DKMTAdapterInfoStruct.@NotNull Buffer adapterInfoBuffer) { var results = new ArrayList(); for (int adapterIndex = adapterInfoBuffer.position(); adapterIndex < adapterInfoBuffer.limit(); adapterIndex++) { @@ -64,7 +63,7 @@ public static List findGraphicsAdapters() { return results; } - private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapterInfoBuffer) { + private static void freeAdapters(D3DKMTAdapterInfoStruct.@NotNull Buffer adapterInfoBuffer) { for (int adapterIndex = adapterInfoBuffer.position(); adapterIndex < adapterInfoBuffer.limit(); adapterIndex++) { var adapterInfo = adapterInfoBuffer.get(adapterIndex); apiCheckError("D3DKMTCloseAdapter", @@ -72,7 +71,7 @@ private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapter } } - private static @Nullable D3DKMT.WDDMAdapterInfo getAdapterInfo(int adapter) { + private static D3DKMT.@Nullable WDDMAdapterInfo getAdapterInfo(int adapter) { int adapterType = queryAdapterType(adapter); if (!isSupportedAdapterType(adapterType)) { @@ -81,8 +80,8 @@ private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapter String adapterName = queryFriendlyName(adapter); - @Nullable String driverFileName = queryDriverFileName(adapter); - @Nullable WindowsFileVersion driverVersion = null; + String driverFileName = queryDriverFileName(adapter); + WindowsFileVersion driverVersion = null; GraphicsAdapterVendor driverVendor = GraphicsAdapterVendor.UNKNOWN; @@ -201,8 +200,18 @@ public String toString() { } } - // Returns (null) if input is (null). - private static String getOpenGlIcdName(String path) { - return FilenameUtils.removeExtension(FilenameUtils.getName(path)); + private static String getOpenGlIcdName(@Nullable String filePath) { + if (filePath == null) { + return null; + } + + var fileName = Paths.get(filePath) + .getFileName(); + + if (fileName == null) { + return null; + } + + return fileName.toString(); } } diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java similarity index 97% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java index de36d44164..ad07d37ea8 100644 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java +++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java @@ -8,8 +8,6 @@ import java.nio.ByteBuffer; -import static org.lwjgl.system.MemoryUtil.*; - // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_enumadapters // typedef struct _D3DKMT_ENUMADAPTERS { // [in] ULONG NumAdapters; diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java similarity index 100% rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java diff --git a/common/src/workarounds/resources/META-INF/MANIFEST.MF b/common/src/boot/resources/META-INF/MANIFEST.MF similarity index 100% rename from common/src/workarounds/resources/META-INF/MANIFEST.MF rename to common/src/boot/resources/META-INF/MANIFEST.MF diff --git a/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java b/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java index 577c762e52..71aea09e74 100644 --- a/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java +++ b/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java @@ -7,7 +7,7 @@ import java.io.IOException; public class LaunchWarn { - private static final String HELP_URL = "https://github.com/CaffeineMC/sodium/wiki/Installation"; + private static final String HELP_URL = "https://link.caffeinemc.net/guides/sodium/installation"; private static final String RICH_MESSAGE = "" + @@ -91,7 +91,7 @@ private static void showRichGraphicalDialog(BrowseUrlHandler browseUrlHandler) { } private static void showFallbackGraphicalDialog() { - // Fallback for Linux, etc users with no "default" browser + // Fallback for Linux, etc. users with no "default" browser showDialogBox(FALLBACK_MESSAGE, WINDOW_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, null); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java index 9dba7b3641..efe539a697 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java @@ -1,18 +1,18 @@ package net.caffeinemc.mods.sodium.client; -import net.caffeinemc.mods.sodium.client.data.fingerprint.FingerprintMeasure; -import net.caffeinemc.mods.sodium.client.data.fingerprint.HashedFingerprint; -import net.caffeinemc.mods.sodium.client.gui.SodiumGameOptions; import net.caffeinemc.mods.sodium.client.console.Console; import net.caffeinemc.mods.sodium.client.console.message.MessageLevel; -import net.minecraft.network.chat.Component; +import net.caffeinemc.mods.sodium.client.data.fingerprint.FingerprintMeasure; +import net.caffeinemc.mods.sodium.client.data.fingerprint.HashedFingerprint; +import net.caffeinemc.mods.sodium.client.gui.SodiumOptions; +import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; public class SodiumClientMod { - private static SodiumGameOptions CONFIG; + private static SodiumOptions OPTIONS; private static final Logger LOGGER = LoggerFactory.getLogger("Sodium"); private static String MOD_VERSION; @@ -20,7 +20,7 @@ public class SodiumClientMod { public static void onInitialization(String version) { MOD_VERSION = version; - CONFIG = loadConfig(); + OPTIONS = loadConfig(); try { updateFingerprint(); @@ -29,12 +29,12 @@ public static void onInitialization(String version) { } } - public static SodiumGameOptions options() { - if (CONFIG == null) { + public static SodiumOptions options() { + if (OPTIONS == null) { throw new IllegalStateException("Config not yet available"); } - return CONFIG; + return OPTIONS; } public static Logger logger() { @@ -45,16 +45,16 @@ public static Logger logger() { return LOGGER; } - private static SodiumGameOptions loadConfig() { + private static SodiumOptions loadConfig() { try { - return SodiumGameOptions.loadFromDisk(); + return SodiumOptions.loadFromDisk(); } catch (Exception e) { LOGGER.error("Failed to load configuration file", e); LOGGER.error("Using default configuration file in read-only mode"); Console.instance().logMessage(MessageLevel.SEVERE, "sodium.console.config_not_loaded", true, 12.5); - var config = SodiumGameOptions.defaults(); + var config = SodiumOptions.defaults(); config.setReadOnly(); return config; @@ -62,10 +62,10 @@ private static SodiumGameOptions loadConfig() { } public static void restoreDefaultOptions() { - CONFIG = SodiumGameOptions.defaults(); + OPTIONS = SodiumOptions.defaults(); try { - SodiumGameOptions.writeToDisk(CONFIG); + SodiumOptions.writeToDisk(OPTIONS); } catch (IOException e) { throw new RuntimeException("Failed to write config file", e); } @@ -97,14 +97,18 @@ private static void updateFingerprint() { if (saved == null || !current.looselyMatches(saved)) { HashedFingerprint.writeToDisk(current.hashed()); - CONFIG.notifications.hasSeenDonationPrompt = false; - CONFIG.notifications.hasClearedDonationButton = false; + OPTIONS.notifications.hasSeenDonationPrompt = false; + OPTIONS.notifications.hasClearedDonationButton = false; try { - SodiumGameOptions.writeToDisk(CONFIG); + SodiumOptions.writeToDisk(OPTIONS); } catch (IOException e) { LOGGER.error("Failed to update config file", e); } } } + + public static boolean allowDebuggingOptions() { + return PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java index 6f9ad8117c..b4ca154078 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java @@ -2,8 +2,6 @@ import net.caffeinemc.mods.sodium.client.console.Console; import net.caffeinemc.mods.sodium.client.console.message.MessageLevel; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.FilePackResources; import net.minecraft.server.packs.PackResources; @@ -38,7 +36,10 @@ public class ResourcePackScanner { "rendertype_translucent.json", "rendertype_tripwire.vsh", "rendertype_tripwire.fsh", - "rendertype_tripwire.json" + "rendertype_tripwire.json", + "rendertype_clouds.vsh", + "rendertype_clouds.fsh", + "rendertype_clouds.json" ); private static final Set SHADER_INCLUDE_BLACKLIST = Set.of( diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java new file mode 100644 index 0000000000..e4f057cf6e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java @@ -0,0 +1,26 @@ +package net.caffeinemc.mods.sodium.client.config; + +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class AnonymousOptionBinding implements OptionBinding { + private final Consumer save; + private final Supplier load; + + public AnonymousOptionBinding(Consumer save, Supplier load) { + this.save = save; + this.load = load; + } + + @Override + public void save(V value) { + this.save.accept(value); + } + + @Override + public V load() { + return this.load.get(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java new file mode 100644 index 0000000000..f3ba1e9a0f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java @@ -0,0 +1,133 @@ +package net.caffeinemc.mods.sodium.client.config; + + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint; +import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder; +import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.config.builder.ConfigBuilderImpl; +import net.caffeinemc.mods.sodium.client.config.structure.Config; +import net.caffeinemc.mods.sodium.client.config.structure.ModOptions; +import net.minecraft.CrashReport; +import net.minecraft.client.Minecraft; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ConfigManager { + public static final String CONFIG_ENTRY_POINT_KEY = "sodium:config_api_user"; + + private record ConfigUser( + Supplier configEntrypoint, + String modId) { + } + public record ModMetadata(String modName, String modVersion) { + } + + private static final Collection configUsers = new ArrayList<>(); + + public static Config CONFIG; + private static Function modInfoFunction; + + public static void setModInfoFunction(Function modInfoFunction) { + ConfigManager.modInfoFunction = modInfoFunction; + } + + public static void registerConfigEntryPoint(String className, String modId) { + Class entryPointClass; + try { + entryPointClass = Class.forName(className); + } catch (ClassNotFoundException e) { + SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class is missing: {}", modId, className); + return; + } + if (!ConfigEntryPoint.class.isAssignableFrom(entryPointClass)) { + SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class is of the wrong type: {}", modId, entryPointClass); + return; + } + + registerConfigEntryPoint(() -> { + try { + Constructor constructor = entryPointClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return (ConfigEntryPoint) constructor.newInstance(); + } catch (ReflectiveOperationException e) { + SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class could not be constructed: {}", modId, entryPointClass); + } + return null; + }, modId); + } + + public static void registerConfigEntryPoint(Supplier entryPoint, String modId) { + configUsers.add(new ConfigUser(entryPoint, modId)); + } + + public static void registerConfigsEarly() { + registerConfigs(ConfigEntryPoint::registerConfigEarly); + } + + public static void registerConfigsLate() { + registerConfigs(ConfigEntryPoint::registerConfigLate); + } + + private static void registerConfigs(BiConsumer registerMethod) { + var configIds = new ObjectOpenHashSet<>(); + ModOptions sodiumModOptions = null; + var modConfigs = new ObjectArrayList(); + + for (ConfigUser configUser : configUsers) { + var entryPoint = configUser.configEntrypoint.get(); + if (entryPoint == null) { + continue; + } + + var builder = new ConfigBuilderImpl(modInfoFunction, configUser.modId); + Collection builtConfigs; + try { + registerMethod.accept(entryPoint, builder); + builtConfigs = builder.build(); + + for (var modConfig : builtConfigs) { + var configId = modConfig.configId(); + if (configIds.contains(configId)) { + throw new IllegalArgumentException("Mod '" + configUser.modId + "' provided a duplicate mod id: " + configId); + } + + configIds.add(configId); + + if (configId.equals("sodium")) { + sodiumModOptions = modConfig; + } else { + modConfigs.add(modConfig); + } + } + } catch (Exception e) { + crashWithMessage("Mod '" + configUser.modId + "' failed while registering config options.", e); + return; + } + } + + modConfigs.sort(Comparator.comparing(ModOptions::name)); + + if (sodiumModOptions == null) { + throw new RuntimeException("Sodium mod config not found"); + } + modConfigs.add(0, sodiumModOptions); + + try { + CONFIG = new Config(modConfigs); + } catch (Exception e) { + crashWithMessage("Failed to build config options", e); + } + } + + private static void crashWithMessage(String message, Exception e) { + Minecraft.crash(null, Minecraft.getInstance().gameDirectory, new CrashReport(message, e)); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java new file mode 100644 index 0000000000..62f656902a --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java @@ -0,0 +1,135 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.caffeinemc.mods.sodium.api.config.structure.BooleanOptionBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.BooleanOption; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +class BooleanOptionBuilderImpl extends StatefulOptionBuilderImpl implements BooleanOptionBuilder { + BooleanOptionBuilderImpl(ResourceLocation id) { + super(id); + } + + @Override + BooleanOption build() { + this.prepareBuild(); + + return new BooleanOption( + this.id, + this.getDependencies(), + this.getName(), + this.getEnabled(), + this.getStorage(), + this.getTooltipProvider(), + this.getImpact(), + this.getFlags(), + this.getDefaultValue(), + this.getControlHiddenWhenDisabled(), + this.getBinding(), + this.getApplyHook()); + } + + @Override + Class getOptionClass() { + return BooleanOption.class; + } + + @Override + public BooleanOptionBuilder setName(Component name) { + super.setName(name); + return this; + } + + @Override + public BooleanOptionBuilder setStorageHandler(StorageEventHandler storage) { + super.setStorageHandler(storage); + return this; + } + + @Override + public BooleanOptionBuilder setTooltip(Component tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public BooleanOptionBuilder setTooltip(Function tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public BooleanOptionBuilder setImpact(OptionImpact impact) { + super.setImpact(impact); + return this; + } + + @Override + public BooleanOptionBuilder setFlags(OptionFlag... flags) { + super.setFlags(flags); + return this; + } + + @Override + public BooleanOptionBuilder setFlags(ResourceLocation... flags) { + super.setFlags(flags); + return this; + } + + @Override + public BooleanOptionBuilder setDefaultValue(Boolean value) { + super.setDefaultValue(value); + return this; + } + + @Override + public BooleanOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) { + super.setDefaultProvider(provider, dependencies); + return this; + } + + @Override + public BooleanOptionBuilder setEnabled(boolean available) { + super.setEnabled(available); + return this; + } + + @Override + public BooleanOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) { + super.setEnabledProvider(provider, dependencies); + return this; + } + + @Override + public BooleanOptionBuilder setControlHiddenWhenDisabled(boolean hidden) { + super.setControlHiddenWhenDisabled(hidden); + return this; + } + + @Override + public BooleanOptionBuilder setBinding(Consumer save, Supplier load) { + super.setBinding(save, load); + return this; + } + + @Override + public BooleanOptionBuilder setBinding(OptionBinding binding) { + super.setBinding(binding); + return this; + } + + @Override + public BooleanOptionBuilder setApplyHook(Consumer hook) { + super.setApplyHook(hook); + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java new file mode 100644 index 0000000000..e0bedd91fe --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java @@ -0,0 +1,44 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.structure.ColorThemeBuilder; +import net.caffeinemc.mods.sodium.client.gui.ColorTheme; +import net.caffeinemc.mods.sodium.client.gui.Colors; + +public class ColorThemeBuilderImpl implements ColorThemeBuilder { + private static final float MIN_THEME_SATURATION = 0.2f; + private static final float MIN_THEME_BRIGHTNESS = 0.55f; + + private int baseTheme; + private int themeHighlight; + private int themeDisabled; + + ColorTheme build() { + if (this.baseTheme == 0) { + throw new IllegalStateException("Base theme must be set"); + } + + this.baseTheme = Colors.constrainColorHSV(this.baseTheme, MIN_THEME_SATURATION, MIN_THEME_BRIGHTNESS); + + if (this.themeHighlight == 0 || this.themeDisabled == 0) { + return new ColorTheme(this.baseTheme); + } else { + this.themeHighlight = Colors.constrainColorHSV(this.themeHighlight, MIN_THEME_SATURATION, MIN_THEME_BRIGHTNESS); + this.themeDisabled = Colors.constrainColorHSV(this.themeDisabled, MIN_THEME_SATURATION, 0); + return new ColorTheme(this.baseTheme, this.themeHighlight, this.themeDisabled); + } + } + + @Override + public ColorThemeBuilder setBaseThemeRGB(int theme) { + this.baseTheme = theme | 0xFF000000; + return this; + } + + @Override + public ColorThemeBuilder setFullThemeRGB(int theme, int themeHighlight, int themeDisabled) { + this.baseTheme = theme | 0xFF000000; + this.themeHighlight = themeHighlight | 0xFF000000; + this.themeDisabled = themeDisabled | 0xFF000000; + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java new file mode 100644 index 0000000000..b9999ed75b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java @@ -0,0 +1,89 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.structure.*; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.client.config.structure.ModOptions; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public class ConfigBuilderImpl implements ConfigBuilder { + private final List pendingModConfigBuilders = new ArrayList<>(1); + + private final Function modInfoFunction; + private final String defaultConfigId; + + public ConfigBuilderImpl(Function modInfoFunction, String defaultConfigId) { + this.modInfoFunction = modInfoFunction; + this.defaultConfigId = defaultConfigId; + } + + public Collection build() { + var configs = new ArrayList(this.pendingModConfigBuilders.size()); + for (var builder : this.pendingModConfigBuilders) { + configs.add(builder.build()); + } + return configs; + } + + @Override + public ModOptionsBuilder registerModOptions(String configId, String name, String version) { + var builder = new ModOptionsBuilderImpl(configId, name, version); + this.pendingModConfigBuilders.add(builder); + return builder; + } + + @Override + public ModOptionsBuilder registerModOptions(String configId) { + var metadata = this.modInfoFunction.apply(configId); + return this.registerModOptions(configId, metadata.modName(), metadata.modVersion()); + } + + @Override + public ModOptionsBuilder registerOwnModOptions() { + return this.registerModOptions(this.defaultConfigId); + } + + @Override + public ColorThemeBuilder createColorTheme() { + return new ColorThemeBuilderImpl(); + } + + @Override + public OptionPageBuilder createOptionPage() { + return new OptionPageBuilderImpl(); + } + + @Override + public ExternalPageBuilder createExternalPage() { + return new ExternalPageBuilderImpl(); + } + + @Override + public OptionGroupBuilder createOptionGroup() { + return new OptionGroupBuilderImpl(); + } + + @Override + public BooleanOptionBuilder createBooleanOption(ResourceLocation id) { + return new BooleanOptionBuilderImpl(id); + } + + @Override + public IntegerOptionBuilder createIntegerOption(ResourceLocation id) { + return new IntegerOptionBuilderImpl(id); + } + + @Override + public > EnumOptionBuilder createEnumOption(ResourceLocation id, Class enumClass) { + return new EnumOptionBuilderImpl<>(id, enumClass); + } + + @Override + public ExternalButtonOptionBuilder createExternalButtonOption(ResourceLocation id) { + return new ExternalButtonOptionBuilderImpl(id); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java new file mode 100644 index 0000000000..0b20c553f0 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java @@ -0,0 +1,204 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.OptionBinding; +import net.caffeinemc.mods.sodium.api.config.option.OptionFlag; +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.caffeinemc.mods.sodium.api.config.structure.EnumOptionBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.EnumOption; +import net.caffeinemc.mods.sodium.client.config.value.ConstantValue; +import net.caffeinemc.mods.sodium.client.config.value.DependentValue; +import net.caffeinemc.mods.sodium.client.config.value.DynamicValue; +import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +class EnumOptionBuilderImpl> extends StatefulOptionBuilderImpl, E> implements EnumOptionBuilder { + private final Class enumClass; + + private DependentValue> allowedValues; + private Function elementNameProvider; + + EnumOptionBuilderImpl(ResourceLocation id, Class enumClass) { + super(id); + this.enumClass = enumClass; + } + + @Override + void validateData() { + super.validateData(); + + Validate.notNull(this.getElementNameProvider(), "Element name provider must be set or enum class must implement TextProvider"); + } + + @Override + EnumOption build() { + if (this.getAllowedValues() == null) { + this.allowedValues = new ConstantValue<>(Set.of(this.enumClass.getEnumConstants())); + } + + if (this.getElementNameProvider() == null && TextProvider.class.isAssignableFrom(this.enumClass)) { + this.elementNameProvider = e -> ((TextProvider) e).getLocalizedName(); + } + + this.prepareBuild(); + + return new EnumOption<>(this.id, + this.getDependencies(), + this.getName(), + this.getEnabled(), + this.getStorage(), + this.getTooltipProvider(), this.getImpact(), + this.getFlags(), + this.getDefaultValue(), + this.getControlHiddenWhenDisabled(), + this.getBinding(), + this.getApplyHook(), + this.getEnumClass(), + this.getAllowedValues(), + this.getElementNameProvider()); + } + + @Override + Class> getOptionClass() { + @SuppressWarnings("unchecked") + Class> clazz = (Class>) (Class) EnumOption.class; + return clazz; + } + + @Override + Collection getDependencies() { + var deps = super.getDependencies(); + deps.addAll(this.getAllowedValues().getDependencies()); + return deps; + } + + Class getEnumClass() { + return this.getFirstNotNull(this.enumClass, EnumOption::getEnumClass); + } + + DependentValue> getAllowedValues() { + return this.getFirstNotNull(this.allowedValues, EnumOption::getAllowedValues); + } + + Function getElementNameProvider() { + return this.getFirstNotNull(this.elementNameProvider, EnumOption::getElementNameProvider); + } + + @Override + public EnumOptionBuilder setName(Component name) { + super.setName(name); + return this; + } + + @Override + public EnumOptionBuilder setElementNameProvider(Function provider) { + this.elementNameProvider = provider; + return this; + } + + @Override + public EnumOptionBuilder setEnabled(boolean available) { + super.setEnabled(available); + return this; + } + + @Override + public EnumOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) { + super.setEnabledProvider(provider, dependencies); + return this; + } + + @Override + public EnumOptionBuilder setStorageHandler(StorageEventHandler storage) { + super.setStorageHandler(storage); + return this; + } + + @Override + public EnumOptionBuilder setTooltip(Component tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public EnumOptionBuilder setTooltip(Function tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public EnumOptionBuilder setImpact(OptionImpact impact) { + super.setImpact(impact); + return this; + } + + @Override + public EnumOptionBuilder setFlags(OptionFlag... flags) { + super.setFlags(flags); + return this; + } + + @Override + public EnumOptionBuilder setFlags(ResourceLocation... flags) { + super.setFlags(flags); + return this; + } + + @Override + public EnumOptionBuilder setDefaultValue(E value) { + super.setDefaultValue(value); + return this; + } + + @Override + public EnumOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) { + super.setDefaultProvider(provider, dependencies); + return this; + } + + @Override + public EnumOptionBuilder setControlHiddenWhenDisabled(boolean hidden) { + super.setControlHiddenWhenDisabled(hidden); + return this; + } + + @Override + public EnumOptionBuilder setBinding(Consumer save, Supplier load) { + super.setBinding(save, load); + return this; + } + + @Override + public EnumOptionBuilder setBinding(OptionBinding binding) { + super.setBinding(binding); + return this; + } + + @Override + public EnumOptionBuilder setApplyHook(Consumer hook) { + super.setApplyHook(hook); + return this; + } + + @Override + public EnumOptionBuilder setAllowedValues(Set allowedValues) { + this.allowedValues = new ConstantValue<>(allowedValues); + return this; + } + + @Override + public EnumOptionBuilder setAllowedValuesProvider(Function> provider, ResourceLocation... dependencies) { + this.allowedValues = new DynamicValue<>(provider, dependencies); + return this; + } + +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java new file mode 100644 index 0000000000..ef74b272ec --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java @@ -0,0 +1,73 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.structure.ExternalButtonOptionBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.ExternalButtonOption; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.util.function.Consumer; +import java.util.function.Function; + +class ExternalButtonOptionBuilderImpl extends StaticOptionBuilderImpl implements ExternalButtonOptionBuilder { + private Consumer currentScreenConsumer; + + ExternalButtonOptionBuilderImpl(ResourceLocation id) { + super(id); + } + + @Override + void validateData() { + super.validateData(); + + Validate.notNull(this.getCurrentScreenConsumer(), "Screen provider must be set"); + } + + @Override + ExternalButtonOption build() { + this.prepareBuild(); + + return new ExternalButtonOption(this.id, this.getDependencies(), this.getName(), this.getEnabled(), this.getTooltip(), this.getCurrentScreenConsumer()); + } + + @Override + Class getOptionClass() { + return ExternalButtonOption.class; + } + + Consumer getCurrentScreenConsumer() { + return this.getFirstNotNull(this.currentScreenConsumer, ExternalButtonOption::getCurrentScreenConsumer); + } + + @Override + public ExternalButtonOptionBuilder setName(Component name) { + super.setName(name); + return this; + } + + @Override + public ExternalButtonOptionBuilder setEnabled(boolean available) { + super.setEnabled(available); + return this; + } + + @Override + public ExternalButtonOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) { + super.setEnabledProvider(provider, dependencies); + return this; + } + + @Override + public ExternalButtonOptionBuilder setTooltip(Component tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public ExternalButtonOptionBuilder setScreenConsumer(Consumer currentScreenConsumer) { + this.currentScreenConsumer = currentScreenConsumer; + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java new file mode 100644 index 0000000000..5c05fd5497 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java @@ -0,0 +1,38 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.structure.ExternalPageBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.ExternalPage; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.function.Consumer; + +public class ExternalPageBuilderImpl extends PageBuilderImpl implements ExternalPageBuilder { + private Consumer currentScreenConsumer; + + @Override + void prepareBuild() { + super.prepareBuild(); + + Validate.notNull(this.currentScreenConsumer, "Screen consumer must not be null"); + } + + @Override + ExternalPage build() { + this.prepareBuild(); + return new ExternalPage(this.name, this.currentScreenConsumer); + } + + @Override + public ExternalPageBuilder setScreenConsumer(Consumer currentScreenConsumer) { + this.currentScreenConsumer = currentScreenConsumer; + return this; + } + + @Override + public ExternalPageBuilderImpl setName(Component name) { + super.setName(name); + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java new file mode 100644 index 0000000000..f2c2873836 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java @@ -0,0 +1,201 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.StorageEventHandler; +import net.caffeinemc.mods.sodium.api.config.option.*; +import net.caffeinemc.mods.sodium.api.config.structure.IntegerOptionBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.IntegerOption; +import net.caffeinemc.mods.sodium.client.config.value.ConstantValue; +import net.caffeinemc.mods.sodium.client.config.value.DependentValue; +import net.caffeinemc.mods.sodium.client.config.value.DynamicValue; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +class IntegerOptionBuilderImpl extends StatefulOptionBuilderImpl implements IntegerOptionBuilder { + private DependentValue validatorProvider; + private ControlValueFormatter valueFormatter; + + IntegerOptionBuilderImpl(ResourceLocation id) { + super(id); + } + + @Override + void validateData() { + super.validateData(); + + Validate.notNull(this.getValidatorProvider(), "Validator provider must be set"); + Validate.notNull(this.getValueFormatter(), "Value formatter must be set"); + } + + @Override + IntegerOption build() { + this.prepareBuild(); + + return new IntegerOption( + this.id, + this.getDependencies(), + this.getName(), + this.getEnabled(), + this.getStorage(), + this.getTooltipProvider(), + this.getImpact(), + this.getFlags(), + this.getDefaultValue(), + this.getControlHiddenWhenDisabled(), + this.getBinding(), + this.getApplyHook(), + this.getValidatorProvider(), + this.getValueFormatter()); + } + + @Override + Collection getDependencies() { + var deps = super.getDependencies(); + deps.addAll(this.getValidatorProvider().getDependencies()); + return deps; + } + + @Override + Class getOptionClass() { + return IntegerOption.class; + } + + DependentValue getValidatorProvider() { + return this.getFirstNotNull(this.validatorProvider, IntegerOption::getValidatorProvider); + } + + ControlValueFormatter getValueFormatter() { + return this.getFirstNotNull(this.valueFormatter, IntegerOption::getValueFormatter); + } + + @Override + public IntegerOptionBuilder setName(Component name) { + super.setName(name); + return this; + } + + @Override + public IntegerOptionBuilder setEnabled(boolean available) { + super.setEnabled(available); + return this; + } + + @Override + public IntegerOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) { + super.setEnabledProvider(provider, dependencies); + return this; + } + + @Override + public IntegerOptionBuilder setStorageHandler(StorageEventHandler storage) { + super.setStorageHandler(storage); + return this; + } + + @Override + public IntegerOptionBuilder setTooltip(Component tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public IntegerOptionBuilder setTooltip(Function tooltip) { + super.setTooltip(tooltip); + return this; + } + + @Override + public IntegerOptionBuilder setImpact(OptionImpact impact) { + super.setImpact(impact); + return this; + } + + @Override + public IntegerOptionBuilder setFlags(OptionFlag... flags) { + super.setFlags(flags); + return this; + } + + @Override + public IntegerOptionBuilder setFlags(ResourceLocation... flags) { + super.setFlags(flags); + return this; + } + + @Override + public IntegerOptionBuilder setDefaultValue(Integer value) { + super.setDefaultValue(value); + return this; + } + + @Override + public IntegerOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) { + super.setDefaultProvider(provider, dependencies); + return this; + } + + @Override + public IntegerOptionBuilder setControlHiddenWhenDisabled(boolean hidden) { + super.setControlHiddenWhenDisabled(hidden); + return this; + } + + @Override + public IntegerOptionBuilder setBinding(Consumer save, Supplier load) { + super.setBinding(save, load); + return this; + } + + @Override + public IntegerOptionBuilder setBinding(OptionBinding binding) { + super.setBinding(binding); + return this; + } + + @Override + public IntegerOptionBuilder setApplyHook(Consumer hook) { + super.setApplyHook(hook); + return this; + } + + @Override + public IntegerOptionBuilder setRange(int min, int max, int step) { + return this.setRange(new Range(min, max, step)); + } + + @Override + public IntegerOptionBuilder setRange(Range range) { + this.validatorProvider = new ConstantValue<>(range); + return this; + } + + @Override + public IntegerOptionBuilder setRangeProvider(Function provider, ResourceLocation... dependencies) { + this.validatorProvider = new DynamicValue<>(provider, dependencies); + return this; + } + + @Override + public IntegerOptionBuilder setValidator(SteppedValidator validator) { + this.validatorProvider = new ConstantValue<>(validator); + return this; + } + + @Override + public IntegerOptionBuilder setValidatorProvider(Function provider, ResourceLocation... dependencies) { + this.validatorProvider = new DynamicValue<>(provider, dependencies); + return this; + } + + @Override + public IntegerOptionBuilder setValueFormatter(ControlValueFormatter formatter) { + this.valueFormatter = formatter; + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java new file mode 100644 index 0000000000..c5a35cff2c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java @@ -0,0 +1,135 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.option.FlagHook; +import net.caffeinemc.mods.sodium.api.config.structure.ColorThemeBuilder; +import net.caffeinemc.mods.sodium.api.config.structure.ModOptionsBuilder; +import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder; +import net.caffeinemc.mods.sodium.api.config.structure.PageBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.*; +import net.caffeinemc.mods.sodium.client.gui.ColorTheme; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +class ModOptionsBuilderImpl implements ModOptionsBuilder { + private final String configId; + private String name; + private String version; + private ColorTheme theme; + private ResourceLocation icon; + private boolean iconMonochrome = true; + private final List pages = new ArrayList<>(); + private List optionOverrides; + private List optionOverlays; + private Collection flagHooks; + + ModOptionsBuilderImpl(String configId, String name, String version) { + this.configId = configId; + this.name = name; + this.version = version; + } + + ModOptions build() { + Validate.notEmpty(this.name, "Name must not be empty"); + Validate.notEmpty(this.version, "Version must not be empty"); + + var overrides = this.optionOverrides == null ? List.of() : this.optionOverrides; + var overlays = this.optionOverlays == null ? List.of() : this.optionOverlays; + + if (this.pages.isEmpty() && overrides.isEmpty() && overlays.isEmpty()) { + throw new IllegalStateException("At least one page, option override, or option overlay must be added"); + } + + // if no theme is specified, pick one pseudo-randomly based on the configId + if (this.theme == null) { + this.theme = ColorTheme.PRESETS[Math.abs(this.configId.hashCode()) % ColorTheme.PRESETS.length]; + } + + return new ModOptions(this.configId, this.name, this.version, this.theme, this.icon, this.iconMonochrome, ImmutableList.copyOf(this.pages), overrides, overlays, this.flagHooks); + } + + @Override + public ModOptionsBuilder setName(String name) { + this.name = name; + return this; + } + + @Override + public ModOptionsBuilder setVersion(String version) { + this.version = version; + return this; + } + + @Override + public ModOptionsBuilder formatVersion(Function versionFormatter) { + this.version = versionFormatter.apply(this.version); + return this; + } + + @Override + public ModOptionsBuilder setColorTheme(ColorThemeBuilder theme) { + this.theme = ((ColorThemeBuilderImpl) theme).build(); + return this; + } + + @Override + public ModOptionsBuilder setIcon(ResourceLocation texture) { + this.icon = texture; + return this; + } + + @Override + public ModOptionsBuilder setNonTintedIcon(ResourceLocation texture) { + this.icon = texture; + this.iconMonochrome = false; + return this; + } + + @Override + public ModOptionsBuilder addPage(PageBuilder builder) { + this.pages.add(((PageBuilderImpl) builder).build()); + return this; + } + + @Override + public ModOptionsBuilder registerOptionReplacement(ResourceLocation target, OptionBuilder replacement) { + var override = new OptionOverride(target, this.configId, ((OptionBuilderImpl) replacement).build()); + if (this.optionOverrides == null) { + this.optionOverrides = new ArrayList<>(); + } + this.optionOverrides.add(override); + return this; + } + + @Override + public ModOptionsBuilder registerOptionOverlay(ResourceLocation target, OptionBuilder overlay) { + var optionOverlay = new OptionOverlay(target, this.configId, ((OptionBuilderImpl) overlay)); + if (this.optionOverlays == null) { + this.optionOverlays = new ArrayList<>(); + } + this.optionOverlays.add(optionOverlay); + return this; + } + + @Override + public ModOptionsBuilder registerFlagHook(BiConsumer, ConfigState> hook, ResourceLocation... triggers) { + return this.registerFlagHook(new FlagHookImpl(hook, List.of(triggers))); + } + + @Override + public ModOptionsBuilder registerFlagHook(FlagHook hook) { + if (this.flagHooks == null) { + this.flagHooks = new ObjectArrayList<>(); + } + this.flagHooks.add(hook); + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java new file mode 100644 index 0000000000..9a63823704 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java @@ -0,0 +1,101 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import net.caffeinemc.mods.sodium.api.config.ConfigState; +import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.Option; +import net.caffeinemc.mods.sodium.client.config.value.ConstantValue; +import net.caffeinemc.mods.sodium.client.config.value.DependentValue; +import net.caffeinemc.mods.sodium.client.config.value.DynamicValue; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.function.Function; + +public abstract class OptionBuilderImpl implements OptionBuilder { + final ResourceLocation id; + + private O baseOption; + + private Component name; + private DependentValue enabled; + + OptionBuilderImpl(ResourceLocation id) { + this.id = id; + } + + abstract O build(); + + abstract Class getOptionClass(); + + public O buildWithBaseOption(Option baseOption) { + Validate.isTrue(this.getOptionClass().isInstance(baseOption), "Base option must be of type %s", this.getOptionClass().getSimpleName()); + + @SuppressWarnings("unchecked") + O castedBaseOption = (O) baseOption; + this.baseOption = castedBaseOption; + + return this.build(); + } + + void validateData() { + Validate.notNull(this.getName(), "Name must be set"); + Validate.notBlank(this.getName().getString(), "Name must not be blank"); + } + + void prepareBuild() { + this.validateData(); + + if (this.getEnabled() == null) { + this.enabled = new ConstantValue<>(true); + } + } + + Collection getDependencies() { + var dependencies = new ObjectLinkedOpenHashSet(); + dependencies.addAll(this.getEnabled().getDependencies()); + return dependencies; + } + + public V getFirstNotNull(V overlayValue, Function extractor) { + if (overlayValue != null) { + return overlayValue; + } else if (this.baseOption != null) { + return extractor.apply(this.baseOption); + } else { + return null; + } + } + + Component getName() { + return this.getFirstNotNull(this.name, Option::getName); + } + + DependentValue getEnabled() { + return this.getFirstNotNull(this.enabled, Option::getEnabled); + } + + @Override + public OptionBuilder setName(Component name) { + Validate.notNull(name, "Argument must not be null"); + + this.name = name; + return this; + } + + @Override + public OptionBuilder setEnabled(boolean available) { + this.enabled = new ConstantValue<>(available); + return this; + } + + @Override + public OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) { + Validate.notNull(provider, "Argument must not be null"); + + this.enabled = new DynamicValue<>(provider, dependencies); + return this; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java new file mode 100644 index 0000000000..c07685ee69 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java @@ -0,0 +1,34 @@ +package net.caffeinemc.mods.sodium.client.config.builder; + +import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder; +import net.caffeinemc.mods.sodium.api.config.structure.OptionGroupBuilder; +import net.caffeinemc.mods.sodium.client.config.structure.Option; +import net.caffeinemc.mods.sodium.client.config.structure.OptionGroup; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.List; + +class OptionGroupBuilderImpl implements OptionGroupBuilder { + private Component name; + private final List

extends EntryWidget { + final P page; + final int scrollTargetStart; + + PageEntryWidget(Dim2i dim, P page, Component label, ColorTheme theme, int scrollTargetStart) { + super(dim, label, true, theme); + this.page = page; + this.scrollTargetStart = scrollTargetStart; + } + } + + private class OptionPageEntryWidget extends PageEntryWidget { + OptionPageEntryWidget(Dim2i dim, Page page, ColorTheme theme, int scrollTargetStart) { + super(dim, page, page.name(), theme, scrollTargetStart); + } + + @Override + public int getScrollTargetStart() { + return this.scrollTargetStart; + } + + @Override + void onAction() { + PageListWidget.this.switchSelectedWidget(this); + PageListWidget.this.parent.jumpToPage(this.page); + } + } + + private class ExternalPageEntryWidget extends PageEntryWidget { + ExternalPageEntryWidget(Dim2i dim, ExternalPage page, ColorTheme theme, int scrollTargetStart) { + super(dim, page, Component.literal(ExternalButtonControl.EXTERNAL_PAGE_PREFIX).append(page.name()), theme, scrollTargetStart); + } + + @Override + void onAction() { + this.page.currentScreenConsumer().accept(PageListWidget.this.parent); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ResetButton.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ResetButton.java new file mode 100644 index 0000000000..e0ea057b96 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ResetButton.java @@ -0,0 +1,87 @@ +package net.caffeinemc.mods.sodium.client.gui.widgets; + +import net.caffeinemc.mods.sodium.client.gui.GuiTint; +import net.caffeinemc.mods.sodium.client.gui.Layout; +import net.caffeinemc.mods.sodium.client.util.Dim2i; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +/** + * Right-aligned reset overlay shown when the parent row is hovered while SHIFT is held. + * It derives is positioning from the parent so it tracks the parent's scroll position automatically. + */ +public class ResetButton extends AbstractWidget { + private static final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("sodium", "textures/gui/reset_button.png"); + private static final int ICON_SIZE = Layout.CONTROL_ICON_SIZE; + private static final int COLOR = 0xFFFF8C30; + + private final AbstractWidget parent; + private final Runnable action; + + public ResetButton(AbstractWidget parent, Runnable action) { + super(new Dim2i(0, 0, Layout.BUTTON_SHORT, 0)); + this.parent = parent; + this.action = action; + } + + public static boolean isShiftHeld() { + return Screen.hasShiftDown(); + } + + public boolean isActive() { + return this.parent.isHovered() && isShiftHeld(); + } + + @Override + public int getX() { + return this.parent.getLimitX() - this.getWidth(); + } + + @Override + public int getY() { + return this.parent.getY(); + } + + @Override + public int getHeight() { + return this.parent.getHeight(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (!this.isActive()) { + return; + } + + final int x = this.getCenterX() - ICON_SIZE / 2; + final int y = this.getCenterY() - ICON_SIZE / 2; + + GuiTint.withTint(COLOR, () -> + graphics.blit(ICON, x, y, ICON_SIZE, ICON_SIZE, 0.0f, 0.0f, ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isShiftHeld() || button != 0) { + return false; + } + + if (!this.parent.isMouseOver(mouseX, mouseY) || !this.isMouseOver(mouseX, mouseY)) { + return false; + } + + this.action.run(); + this.playClickSound(); + return true; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + // Only reachable via SHIFT-hover + click. + return null; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollableTooltip.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollableTooltip.java new file mode 100644 index 0000000000..b3a9a45f7e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollableTooltip.java @@ -0,0 +1,276 @@ +package net.caffeinemc.mods.sodium.client.gui.widgets; + +import net.caffeinemc.mods.sodium.api.config.option.OptionImpact; +import net.caffeinemc.mods.sodium.client.gui.Colors; +import net.caffeinemc.mods.sodium.client.gui.GuiTint; +import net.caffeinemc.mods.sodium.client.gui.Layout; +import net.caffeinemc.mods.sodium.client.gui.options.control.ControlElement; +import net.caffeinemc.mods.sodium.client.util.Dim2i; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector2i; + +import java.util.ArrayList; +import java.util.List; + +// TODO: is narration of the tooltip already handled by the screen or is there no narration at all? +public class ScrollableTooltip { + private static final ResourceLocation ARROW_TEXTURE = ResourceLocation.fromNamespaceAndPath("sodium", "textures/gui/tooltip_arrows.png"); + private static final int ARROW_WIDTH = 5; + private static final int SPRITE_WIDTH = 10; + private static final int ARROW_HEIGHT = 9; + + private static final int TEXT_HORIZONTAL_PADDING = Layout.INNER_MARGIN - 1; + private static final int TEXT_VERTICAL_PADDING = TEXT_HORIZONTAL_PADDING; + + private final Font font = Minecraft.getInstance().font; + private ControlElement hoveredElement; + private ScrollbarWidget scrollbar; + private final Vector2i contentSize = new Vector2i(); + private Dim2i visibleDim; + private boolean overlayMode; + private final List content = new ArrayList<>(); + private final TooltipParent parent; + private Dim2i tooltipArea; // area for the tooltip to be within + private final Vector2i reservedArea = new Vector2i(); // area reserved for action buttons + + public ScrollableTooltip(TooltipParent parent) { + this.parent = parent; + } + + public interface TooltipParent { + T addRenderableWidget(T guiEventListener); + void removeWidget(GuiEventListener guiEventListener); + } + + public void setTooltipArea(Dim2i area) { + this.tooltipArea = area; + } + + public void onControlHover(ControlElement hovered, int mouseX, int mouseY) { + if (hovered != null) { + this.hoveredElement = hovered; + + if (this.scrollbar != null) { + this.parent.removeWidget(this.scrollbar); + this.scrollbar = null; + } + + // re-flow the content if we need a scrollbar, which takes up some width + if (this.positionTooltip(false)) { + this.positionTooltip(true); + + this.scrollbar = this.parent.addRenderableWidget(new ScrollbarWidget(new Dim2i( + this.visibleDim.getLimitX() - Layout.SCROLLBAR_WIDTH, + this.visibleDim.y(), + Layout.SCROLLBAR_WIDTH, + this.visibleDim.height() + ), false, true)); + this.scrollbar.setScrollbarContext(this.visibleDim.height(), this.contentSize.y()); + } + } else if (this.hoveredElement != null) { + this.positionTooltip(this.scrollbar != null); + + // handle the space between options and their tooltip + if ((mouseX < this.hoveredElement.getLimitX() || mouseX >= this.visibleDim.x() || + mouseY < this.hoveredElement.getY() || mouseY >= this.hoveredElement.getLimitY()) && + !this.visibleDim.containsCursor(mouseX, mouseY)) { + this.hoveredElement = null; + + if (this.scrollbar != null) { + this.parent.removeWidget(this.scrollbar); + this.scrollbar = null; + } + } + } + } + + private int getLineHeight() { + return this.font.lineHeight + Layout.TEXT_LINE_SPACING; + } + + private int generateTooltipContent(int boxWidth, boolean needsScrolling) { + int textWidth = boxWidth - TEXT_HORIZONTAL_PADDING * 2; + if (needsScrolling) { + textWidth -= Layout.SCROLLBAR_WIDTH; + } + + var option = this.hoveredElement.getOption(); + + this.content.clear(); + this.content.addAll(this.font.split(option.getTooltip(), textWidth)); + + OptionImpact impact = option.getImpact(); + if (impact != null) { + var impactText = Component.translatable("sodium.options.performance_impact_string", impact.getName()); + this.content.addAll(this.font.split(impactText.withStyle(ChatFormatting.GRAY), textWidth)); + } + + return this.content.size() * this.getLineHeight() - Layout.TEXT_LINE_SPACING + TEXT_VERTICAL_PADDING * 2; + } + + private boolean positionTooltip(boolean needsScrolling) { + int defaultBoxWidth = Math.min(this.tooltipArea.getLimitX() - this.tooltipArea.x(), Layout.MAX_TOOLTIP_WIDTH); + int defaultBoxY = this.hoveredElement.getY(); + int defaultBoxX = this.tooltipArea.x(); + + int boxWidth = 0, boxX = 0, boxY = 0; + boolean fixedBoxY = false; + int boxYCutoff = this.tooltipArea.getLimitY(); + + this.overlayMode = defaultBoxWidth < Layout.MIN_TOOLTIP_WIDTH; + + if (!this.overlayMode) { + // Hovered element above the area so tooltip has full width, needs to be up high enough to not intersect with the area + if (this.hoveredElement.getLimitY() < this.reservedArea.y) { + boxWidth = defaultBoxWidth; + boxX = defaultBoxX; + boxY = defaultBoxY; + + // set the y cutoff to make sure the bottom of the tooltip is moved up above the reserved area + boxYCutoff = this.reservedArea.y; + } + + // Hovered element left of the area but enough space to show tooltip with reduced width + else if (this.tooltipArea.x() < this.reservedArea.x) { + int availableWidth = this.reservedArea.x - this.tooltipArea.x(); + + if (availableWidth >= Layout.MIN_TOOLTIP_WIDTH) { + boxWidth = Math.min(availableWidth, Layout.MAX_TOOLTIP_WIDTH); + boxX = defaultBoxX; + boxY = defaultBoxY; + } else { + this.overlayMode = true; + } + } + + // default non-overlay positioning + else { + boxWidth = defaultBoxWidth; + boxX = defaultBoxX; + boxY = defaultBoxY; + } + } + + if (this.overlayMode) { + // in overlay mode the tooltip is shown on top of the options list, either above or below + boxWidth = this.hoveredElement.getWidth() - 2 * Layout.TOOLTIP_OUTER_MARGIN; + boxX = this.hoveredElement.getX() + Layout.TOOLTIP_OUTER_MARGIN; + + // place the content above or below the hovered element depending on which side has more space + int spaceAbove = this.hoveredElement.getY() - this.tooltipArea.y(); + int spaceBelow = this.tooltipArea.getLimitY() - this.hoveredElement.getLimitY() - Layout.TOOLTIP_OUTER_MARGIN; + if (spaceBelow >= spaceAbove) { + boxY = this.hoveredElement.getLimitY() + Layout.TOOLTIP_OUTER_MARGIN; + boxYCutoff = this.tooltipArea.getLimitY() - Layout.TOOLTIP_OUTER_MARGIN; + + // fix the box's upper y position since moving it up would cause it to overlap the hovered element + fixedBoxY = true; + } else { + boxY = this.hoveredElement.getY() - Layout.TOOLTIP_OUTER_MARGIN; + boxYCutoff = this.hoveredElement.getY() - Layout.TOOLTIP_OUTER_MARGIN; + // it automatically gets moved up as far as necessary later + } + } + + int contentHeight = this.generateTooltipContent(boxWidth, needsScrolling); + int boxYLimit = boxY + contentHeight; + + if (!fixedBoxY) { + // If the box is going to be cut off on the Y-axis, move it back up the difference + if (boxYLimit > boxYCutoff) { + boxY -= boxYLimit - boxYCutoff; + } + + // prevent it from moving up further than the tooltip safe area + if (boxY < this.tooltipArea.y()) { + boxY = this.tooltipArea.y(); + } + } + + this.contentSize.set(boxWidth, contentHeight); + + int maxVisibleHeight = boxYCutoff - boxY; + int visibleHeight = Math.min(contentHeight, maxVisibleHeight); + this.visibleDim = new Dim2i(boxX, boxY, boxWidth, visibleHeight); + + return contentHeight > maxVisibleHeight; + } + + public void render(@NotNull GuiGraphics graphics) { + if (this.hoveredElement == null) { + return; + } + + // 1.21.1 equivalent of more modern GuiGraphics#nextStratum + graphics.pose().pushPose(); + graphics.pose().translate(0.0f, 0.0f, 400.0f); + try { + this.renderInternal(graphics); + } finally { + graphics.pose().popPose(); + } + } + + private void renderInternal(@NotNull GuiGraphics graphics) { + if (!this.overlayMode) { + // draw small triangular arrow attached to the side of the tooltip box pointing at the hovered element, in the margin between the hovered element and the tooltip box + int arrowX = this.visibleDim.x() - ARROW_WIDTH; + int arrowY = this.hoveredElement.getCenterY() - (ARROW_HEIGHT / 2); + + // constrain the arrow to be within the tooltip area + arrowY = Math.max(arrowY, this.tooltipArea.y()); + int arrowYConstrained = Math.min(arrowY + ARROW_HEIGHT, this.tooltipArea.getLimitY()) - ARROW_HEIGHT; + + GuiTint.withTint(Colors.BACKGROUND_LIGHT, () -> + graphics.blit(ARROW_TEXTURE, arrowX, arrowYConstrained, ARROW_WIDTH, 0, ARROW_WIDTH, ARROW_HEIGHT, SPRITE_WIDTH, ARROW_HEIGHT)); + GuiTint.withTint(Colors.BACKGROUND_DEFAULT, () -> + graphics.blit(ARROW_TEXTURE, arrowX, arrowYConstrained, 0, 0, ARROW_WIDTH, ARROW_HEIGHT, SPRITE_WIDTH, ARROW_HEIGHT)); + } + + int lineHeight = this.getLineHeight(); + + int scrollAmount = 0; + if (this.scrollbar != null) { + scrollAmount = this.scrollbar.getScrollAmount(); + } + + var backgroundColor = this.overlayMode ? Colors.BACKGROUND_OVERLAY : Colors.BACKGROUND_LIGHT; + + graphics.enableScissor(this.visibleDim.x(), this.visibleDim.y(), this.visibleDim.getLimitX(), this.visibleDim.getLimitY()); + graphics.fill(this.visibleDim.x(), this.visibleDim.y(), this.visibleDim.getLimitX(), this.visibleDim.getLimitY(), backgroundColor); + for (int i = 0; i < this.content.size(); i++) { + graphics.drawString(this.font, this.content.get(i), + this.visibleDim.x() + TEXT_HORIZONTAL_PADDING, this.visibleDim.y() + TEXT_VERTICAL_PADDING + (i * lineHeight) - scrollAmount, + Colors.FOREGROUND); + } + graphics.disableScissor(); + } + + public boolean mouseScrolled(double d, double e, double amount) { + if (this.visibleDim != null && this.visibleDim.containsCursor(d, e) && this.scrollbar != null) { + this.scrollbar.scroll((int) (-amount * 10)); + return true; + } + return false; + } + + /** + * Sets the position of the top left corner of the reserved area that extends to the right and to the bottom to the screen edges. + * + * @param x The x coordinate of the top left corner of the reserved area + * @param y The y coordinate of the top left corner of the reserved area + */ + public void setReservedAreaTopLeftCorner(int x, int y) { + this.reservedArea.set(x - Layout.TOOLTIP_OUTER_MARGIN, y - Layout.TOOLTIP_OUTER_MARGIN); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollbarWidget.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollbarWidget.java new file mode 100644 index 0000000000..2400b8dce9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/ScrollbarWidget.java @@ -0,0 +1,178 @@ +package net.caffeinemc.mods.sodium.client.gui.widgets; + +import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.client.util.Dim2i; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.IntConsumer; + +public class ScrollbarWidget extends AbstractWidget { + private static final int COLOR = ColorABGR.pack(50, 50, 50, 150); + private static final int HIGHLIGHT_COLOR = ColorABGR.pack(100, 100, 100, 150); + + private final boolean horizontal; + private final boolean alwaysShow; + + private int visible; + private int total; + + private int scrollAmount; + private long lastScrollTime; + private boolean dragging; + private final IntConsumer onScrollChange; + + public ScrollbarWidget(Dim2i dim2i, IntConsumer onScrollChange) { + this(dim2i, false, false, onScrollChange); + } + + public ScrollbarWidget(Dim2i dim2i, boolean horizontal, boolean alwaysShow) { + this(dim2i, horizontal, alwaysShow, null); + } + + public ScrollbarWidget(Dim2i dim2i, boolean horizontal, boolean alwaysShow, IntConsumer onScrollChange) { + super(dim2i); + this.horizontal = horizontal; + this.alwaysShow = alwaysShow; + this.onScrollChange = onScrollChange; + } + + public void setScrollbarContext(int visible, int total) { + this.visible = visible; + this.total = total; + this.setScrollAndNotify(Math.max(0, Math.min(total - visible, this.scrollAmount))); + } + + public void setScrollbarContext(int total) { + this.setScrollbarContext(this.horizontal ? this.getWidth() : this.getHeight(), total); + } + + public boolean canScroll() { + return this.total > this.visible; + } + + public void scroll(int amount) { + this.scrollTo(this.scrollAmount + amount); + } + + public void scrollTo(int target) { + if (this.setScrollAndNotify(Math.max(0, Math.min(this.total - this.visible, target)))) { + this.lastScrollTime = System.currentTimeMillis(); + } + } + + public int getScrollAmount() { + return this.scrollAmount; + } + + private boolean setScrollAndNotify(int newScrollAmount) { + if (newScrollAmount != this.scrollAmount) { + this.scrollAmount = newScrollAmount; + if (this.onScrollChange != null) { + this.onScrollChange.accept(this.scrollAmount); + } + return true; + } + return false; + } + + @Override + public void render(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (!this.canScroll()) { + return; + } + boolean isMouseOver = this.isMouseOver(mouseX, mouseY); + if (isMouseOver) { + this.lastScrollTime = Math.max(this.lastScrollTime, System.currentTimeMillis() - 500); + } + long time = System.currentTimeMillis(); + long scrollTimeDiff = time - this.lastScrollTime; + if (this.alwaysShow || isMouseOver || this.dragging || scrollTimeDiff < 1000) { + graphics.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), COLOR); + int x1, y1, x2, y2; + if (this.horizontal) { + x1 = this.getX() + this.getHighlightStart(this.getWidth()); + y1 = this.getY(); + x2 = x1 + this.getHighlightLength(this.getWidth()); + y2 = y1 + this.getHeight(); + } else { + x1 = this.getX(); + y1 = this.getY() + this.getHighlightStart(this.getHeight()); + x2 = x1 + this.getWidth(); + y2 = y1 + this.getHighlightLength(this.getHeight()); + } + graphics.fill(x1, y1, x2, y2, HIGHLIGHT_COLOR); + } + } + + private boolean isMouseOverHighlight(double mouseX, double mouseY) { + int x1, y1, x2, y2; + if (this.horizontal) { + x1 = this.getX() + this.getHighlightStart(this.getWidth()); + y1 = this.getY(); + x2 = x1 + this.getHighlightLength(this.getWidth()); + y2 = y1 + this.getHeight(); + } else { + x1 = this.getX(); + y1 = this.getY() + this.getHighlightStart(this.getHeight()); + x2 = x1 + this.getWidth(); + y2 = y1 + this.getHighlightLength(this.getHeight()); + } + return mouseX >= x1 && mouseX <= x2 && mouseY >= y1 && mouseY <= y2; + } + + private int getHighlightStart(int length) { + return (int) Math.round(((double) this.scrollAmount / this.total) * length); + } + + private int getHighlightLength(int length) { + return (int) Math.round(((double) this.visible / this.total) * length); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.isMouseOver(mouseX, mouseY) || !this.canScroll()) { + return false; + } + if (this.isMouseOverHighlight(mouseX, mouseY)) { + this.dragging = true; + } else { + if (this.horizontal) { + this.scroll(mouseX > this.getHighlightStart(this.getWidth()) ? this.getWidth() : -this.getWidth()); + } else { + this.scroll(mouseY > this.getHighlightStart(this.getHeight()) ? this.getHeight() : -this.getHeight()); + } + } + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.dragging = false; + this.lastScrollTime = Math.max(this.lastScrollTime, System.currentTimeMillis() - 500); + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (this.dragging) { + this.scroll((int) Math.round(this.horizontal ? deltaX : deltaY * ((double) this.total / this.visible))); + return true; + } + return false; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + // no narration + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + return null; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/SearchWidget.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/SearchWidget.java new file mode 100644 index 0000000000..58335b1b0e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/widgets/SearchWidget.java @@ -0,0 +1,205 @@ +package net.caffeinemc.mods.sodium.client.gui.widgets; + +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.client.config.search.SearchQuerySession; +import net.caffeinemc.mods.sodium.client.config.structure.Option; +import net.caffeinemc.mods.sodium.client.gui.ButtonTheme; +import net.caffeinemc.mods.sodium.client.gui.Colors; +import net.caffeinemc.mods.sodium.client.gui.Layout; +import net.caffeinemc.mods.sodium.client.util.Dim2i; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Consumer; + +public class SearchWidget extends AbstractParentWidget { + // maximum distance from its original position that a search result can be moved to improve grouping + private static final int MAX_ORDER_DIST_ERROR = 2; + + private static final ButtonTheme CLEAR_BUTTON_THEME = new ButtonTheme( + Colors.FOREGROUND, Colors.FOREGROUND, Colors.FOREGROUND_DISABLED, + Colors.BACKGROUND_MEDIUM, Colors.BACKGROUND_LIGHT, Colors.BACKGROUND_LIGHT); + + private final Consumer> onSearchResults; + private final SearchQuerySession searchQuerySession; + private String query = ""; + + private EditBox searchBox; + private FlatButtonWidget clearButton; + private int lastRebuildWidth = -1; + + public SearchWidget(Consumer> onSearchResults, Dim2i dim) { + super(dim); + this.onSearchResults = onSearchResults; + this.searchQuerySession = ConfigManager.CONFIG.startSearchQuery(); + } + + public void updateWidgetWidth(int width) { + if (width != this.lastRebuildWidth) { + this.lastRebuildWidth = width; + this.rebuildForWidth(width); + } + } + + private void rebuildForWidth(int width) { + this.clearChildren(); + + int x = this.getX(); + int y = this.getY(); + + int searchBoxWidth = width - Layout.BUTTON_SHORT; + this.clearButton = new FlatButtonWidget( + new Dim2i(x + searchBoxWidth, y, Layout.BUTTON_SHORT, Layout.BUTTON_SHORT), + Component.literal("×"), + this::clearSearch, + true, + false, + CLEAR_BUTTON_THEME + ); + + this.searchBox = new EditBox( + this.font, + x + Layout.INNER_MARGIN, + y + Layout.BUTTON_SHORT / 2 - this.font.lineHeight / 2, + searchBoxWidth - Layout.BUTTON_SHORT, + Layout.BUTTON_SHORT, + Component.translatable("sodium.options.search") + ); + + this.searchBox.setMaxLength(200); + this.searchBox.setBordered(false); + this.searchBox.setResponder(this::triggerSearch); + this.searchBox.setHint( + Component.translatable("sodium.options.search.hint") + .withStyle(Style.EMPTY.withColor(ChatFormatting.GRAY))); + + this.addChild(this.searchBox); + this.addChild(this.clearButton); + + this.updateClearButtonVisibility(); + } + + private void updateClearButtonVisibility() { + this.clearButton.setVisible(!this.query.isEmpty()); + } + + private void clearSearch() { + this.searchBox.setValue(""); + this.query = ""; + this.updateClearButtonVisibility(); + this.search(); + this.setFocused(null); + } + + private void triggerSearch(String text) { + if (text.equals(this.query)) { + return; + } + + this.query = text.stripLeading(); + this.updateClearButtonVisibility(); + this.search(); + } + + @SuppressWarnings("unchecked") // we manually check the elements + private void search() { + var results = this.searchQuerySession.getSearchResults(this.query); + + // assert assumption of the result type + for (int i = 0; i < results.size(); i++) { + var result = results.get(i); + result.setResultIndex(i); + + if (!(result instanceof Option.OptionNameSource)) { + throw new UnsupportedOperationException("Unsupported search text source type: " + result.getClass().getName()); + } + } + + List typedResults = (List) results; + + this.improveGrouping(typedResults); + this.onSearchResults.accept(typedResults); + } + + private void improveGrouping(List searchResults) { + // move search results around a little to group them better + var length = searchResults.size(); + for (int i = 1; i < length - 1; i++) { + // if the next result would fit better to the previous one than this one, swap current and next + var prev = searchResults.get(i - 1); + var curr = searchResults.get(i); + var next = searchResults.get(i + 1); + + // check that switching current and next doesn't introduce too much of an ordering error + if (Math.abs(i - prev.getResultIndex()) > MAX_ORDER_DIST_ERROR || + Math.abs(i + 1 - next.getResultIndex()) > MAX_ORDER_DIST_ERROR) { + continue; + } + + var prevCurrScore = this.getGroupScore(prev, curr); + var prevNextScore = this.getGroupScore(prev, next); + + if (prevNextScore > prevCurrScore) { + searchResults.set(i, next); + searchResults.set(i + 1, curr); + } + } + } + + private int getGroupScore(Option.OptionNameSource a, Option.OptionNameSource b) { + if (a.getModOptions() != b.getModOptions()) { + return 0; + } + if (a.getPage() != b.getPage()) { + return 1; + } + if (a.getOptionGroup() != b.getOptionGroup()) { + return 2; + } + return 3; + } + + @Override + public void render(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float delta) { + graphics.fill(this.getX(), this.getY(), this.getX() + this.lastRebuildWidth, this.getLimitY(), Colors.BACKGROUND_DEFAULT); + + this.searchBox.render(graphics, mouseX, mouseY, delta); + this.clearButton.render(graphics, mouseX, mouseY, delta); + + super.render(graphics, mouseX, mouseY, delta); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE && this.getFocused() == this.searchBox) { + this.clearSearch(); + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + return this.searchBox.charTyped(codePoint, modifiers); + } + + public boolean isSearching() { + return this.searchBox.isFocused(); + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + + // on focus, focus the search box for typing + if (focused) { + this.setFocused(this.searchBox); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/ColorProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/ColorProvider.java index f416a134af..6dd0a5ce8a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/ColorProvider.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/ColorProvider.java @@ -15,6 +15,7 @@ public interface ColorProvider { * @param state The state of the object being colorized * @param quad The quad geometry which should be colorized * @param output The output array of vertex colors (in ABGR format) + * @param smooth Whether the colors should be blended across vertices */ - void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, T state, ModelQuadView quad, int[] output); + void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, T state, ModelQuadView quad, int[] output, boolean smooth); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/DefaultColorProviders.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/DefaultColorProviders.java index 848896141b..15b210e138 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/DefaultColorProviders.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/color/DefaultColorProviders.java @@ -7,7 +7,7 @@ import net.minecraft.client.renderer.BiomeColors; import net.minecraft.core.BlockPos; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.material.FluidState; + import java.util.Arrays; public class DefaultColorProviders { @@ -49,7 +49,7 @@ private VanillaAdapter(BlockColor color) { } @Override - public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, BlockState state, ModelQuadView quad, int[] output) { + public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, BlockState state, ModelQuadView quad, int[] output, boolean smooth) { Arrays.fill(output, 0xFF000000 | this.color.getColor(state, slice, pos, quad.getColorIndex())); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/LightPipeline.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/LightPipeline.java index a4c9e8c268..e2ecdddaec 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/LightPipeline.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/LightPipeline.java @@ -4,7 +4,6 @@ import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.world.level.material.FluidState; /** * Light pipelines allow model quads for any location in the level to be lit using various backends, including fluids diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/data/LightDataAccess.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/data/LightDataAccess.java index 0ded5bc8c9..d78b2b5efa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/data/LightDataAccess.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/data/LightDataAccess.java @@ -12,10 +12,10 @@ /** * 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 an 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 integer contains the following fields: * - BL: World block light, encoded as a 4-bit unsigned integer * - SL: World sky light, encoded as a 4-bit unsigned integer @@ -25,7 +25,7 @@ * - OP: Block opacity test, true if opaque * - FO: Full cube opacity test, true if opaque full cube * - FC: Full cube test, true if full cube - * + *

* You can use the various static pack/unpack methods to extract these values in a usable format. */ public abstract class LightDataAccess { @@ -88,13 +88,7 @@ protected int compute(int x, int y, int z) { } } - // FIX: Do not apply AO from blocks that emit light - float ao; - if (lu == 0) { - ao = state.getShadeBrightness(level, pos); - } else { - ao = 1.0f; - } + float ao = state.getShadeBrightness(level, pos); return packFC(fc) | packFO(fo) | packOP(op) | packEM(em) | packAO(ao) | packLU(lu) | packSL(sl) | packBL(bl); } @@ -169,7 +163,7 @@ public static boolean unpackFC(int word) { * Computes the combined lightmap using block light, sky light, and luminance values. * *

This method's logic is equivalent to - * {@link LevelRenderer#getLightColor(BlockAndTintGetter, BlockPos)}, but without the + * {@link LevelRenderer#getLightColor(net.minecraft.world.level.BlockAndTintGetter, BlockPos)}, but without the * emissive check. */ public static int getLightmap(int word) { @@ -181,7 +175,7 @@ public static int getLightmap(int word) { * the {@link LightTexture#FULL_BRIGHT fullbright lightmap} if emissive. * *

This method's logic is equivalent to - * {@link LevelRenderer#getLightColor(BlockAndTintGetter, BlockPos)}. + * {@link LevelRenderer#getLightColor(net.minecraft.world.level.BlockAndTintGetter, BlockPos)}. */ public static int getEmissiveLightmap(int word) { if (unpackEM(word)) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/flat/FlatLightPipeline.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/flat/FlatLightPipeline.java index 314605fb05..589697e5b1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/flat/FlatLightPipeline.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/flat/FlatLightPipeline.java @@ -12,7 +12,6 @@ import net.minecraft.core.Direction; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.material.FluidState; import java.util.Arrays; @@ -38,14 +37,14 @@ public void calculate(ModelQuadView quad, BlockPos pos, QuadLightData out, Direc // To match vanilla behavior, use the cull face if it exists/is available if (cullFace != null) { - lightmap = getOffsetLightmap(pos, cullFace); + lightmap = this.getOffsetLightmap(pos, cullFace); Arrays.fill(out.br, this.lightCache.getLevel().getShade(lightFace, shade)); } 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 && unpackFC(this.lightCache.get(pos)))) { - lightmap = getOffsetLightmap(pos, lightFace); + lightmap = this.getOffsetLightmap(pos, lightFace); Arrays.fill(out.br, this.lightCache.getLevel().getShade(lightFace, shade)); } else { lightmap = getEmissiveLightmap(this.lightCache.get(pos)); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/AoFaceData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/AoFaceData.java index 2fca84b4d6..a7f413b8cb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/AoFaceData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/AoFaceData.java @@ -1,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.model.light.smooth; import net.caffeinemc.mods.sodium.client.model.light.data.LightDataAccess; -import net.minecraft.client.renderer.LightTexture; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -237,30 +236,56 @@ private static int packLight(float sl, float bl) { private static int calculateCornerBrightness(int a, int b, int c, int d, boolean aem, boolean bem, boolean cem, boolean dem) { // 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)) { + int ab = a & 0xFF; + int bb = b & 0xFF; + int cb = c & 0xFF; + int db = d & 0xFF; + if ((ab == 0) || (bb == 0) || (cb == 0) || (db == 0)) { // Find the minimum value between all corners - final int min = minNonZero(minNonZero(a, b), minNonZero(c, d)); + final int min = minNonZero(minNonZero(ab, bb), minNonZero(cb, db)); // Normalize the corner values - a = Math.max(a, min); - b = Math.max(b, min); - c = Math.max(c, min); - d = Math.max(d, min); + ab = Math.max(ab, min); + bb = Math.max(bb, min); + cb = Math.max(cb, min); + db = Math.max(db, min); } + int as = a & 0xFF0000; + int bs = b & 0xFF0000; + int cs = c & 0xFF0000; + int ds = d & 0xFF0000; + if ((as == 0) || (bs == 0) || (cs == 0) || (ds == 0)) { + // Find the minimum value between all corners + final int min = minNonZero(minNonZero(as, bs), minNonZero(cs, ds)); + + // Normalize the corner values + as = Math.max(as, min); + bs = Math.max(bs, min); + cs = Math.max(cs, min); + ds = Math.max(ds, min); + } + a = ab | as; + b = bb | bs; + c = cb | cs; + d = db | ds; // FIX: Apply the fullbright lightmap from emissive blocks at the very end so it cannot influence // the minimum lightmap and produce incorrect results (for example, sculk sensors in a dark room) if (aem) { - a = LightTexture.FULL_BRIGHT; + a &= 0xFF0000; + a |= 0xF0; } if (bem) { - b = LightTexture.FULL_BRIGHT; + b &= 0xFF0000; + b |= 0xF0; } if (cem) { - c = LightTexture.FULL_BRIGHT; + c &= 0xFF0000; + c |= 0xF0; } if (dem) { - d = LightTexture.FULL_BRIGHT; + d &= 0xFF0000; + d |= 0xF0; } return ((a + b + c + d) >> 2) & 0xFF00FF; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java index 781f5780c2..570a2fd70d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java @@ -9,13 +9,12 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.util.Mth; -import net.minecraft.world.level.material.FluidState; import org.joml.Vector3f; /** * 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: - * + *

* - 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-138211) * - Inset block faces are correctly shaded by their neighbors, fixing a number of problems with non-full blocks such as @@ -23,9 +22,9 @@ * - Blocks next to emissive blocks are too bright (MC-260989) * - 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 @@ -102,6 +101,8 @@ public void calculate(ModelQuadView quad, BlockPos pos, QuadLightData out, Direc private void applyAlignedFullFace(AoNeighborInfo neighborInfo, BlockPos pos, Direction dir, QuadLightData out, boolean shade) { AoFaceData faceData = this.getCachedFaceData(pos, dir, true, shade); neighborInfo.mapCorners(faceData.lm, faceData.ao, out.lm, out.br); + + this.applyAmbientLighting(out.br, dir, shade); } /** @@ -119,6 +120,8 @@ private void applyAlignedPartialFace(AoNeighborInfo neighborInfo, ModelQuadView neighborInfo.calculateCornerWeights(cx, cy, cz, weights); this.applyAlignedPartialFaceVertex(pos, dir, weights, i, out, true, shade); } + + this.applyAmbientLighting(out.br, dir, shade); } /** @@ -150,6 +153,8 @@ private void applyParallelFace(AoNeighborInfo neighborInfo, ModelQuadView quad, this.applyInsetPartialFaceVertex(pos, dir, depth, 1.0f - depth, weights, i, out, shade); } } + + this.applyAmbientLighting(out.br, dir, shade); } /** @@ -178,6 +183,8 @@ private void applyNonParallelFace(AoNeighborInfo neighborInfo, ModelQuadView qua this.applyInsetPartialFaceVertex(pos, dir, depth, 1.0f - depth, weights, i, out, shade); } } + + this.applyAmbientLighting(out.br, dir, shade); } private void applyAlignedPartialFaceVertex(BlockPos pos, Direction dir, float[] w, int i, QuadLightData out, boolean offset, boolean shade) { @@ -225,13 +232,13 @@ private AoFaceData gatherInsetFace(ModelQuadView quad, BlockPos blockPos, int ve final float w1 = AoNeighborInfo.get(lightFace).getDepth(quad.getX(vertexIndex), quad.getY(vertexIndex), quad.getZ(vertexIndex)); if (Mth.equal(w1, 0)) { - return getCachedFaceData(blockPos, lightFace, true, shade); + return this.getCachedFaceData(blockPos, lightFace, true, shade); } else if (Mth.equal(w1, 1)) { - return getCachedFaceData(blockPos, lightFace, false, shade); + return this.getCachedFaceData(blockPos, lightFace, false, shade); } else { - tmpFace.reset(); + this.tmpFace.reset(); final float w0 = 1 - w1; - return AoFaceData.weightedMean(getCachedFaceData(blockPos, lightFace, true, shade), w0, getCachedFaceData(blockPos, lightFace, false, shade), w1, tmpFace); + return AoFaceData.weightedMean(this.getCachedFaceData(blockPos, lightFace, true, shade), w0, this.getCachedFaceData(blockPos, lightFace, false, shade), w1, this.tmpFace); } } @@ -246,7 +253,7 @@ private void applyIrregularFace(BlockPos blockPos, ModelQuadView quad, QuadLight for (int i = 0; i < 4; i++) { // TODO: Avoid this if the accurate normal is the face normal - Vector3f normal = NormI8.unpack(quad.getAccurateNormal(i), vertexNormal); + Vector3f normal = NormI8.unpack(quad.getAccurateNormal(i), this.vertexNormal); float ao = 0, sky = 0, block = 0, maxAo = 0; float maxSky = 0, maxBlock = 0; @@ -254,10 +261,10 @@ private void applyIrregularFace(BlockPos blockPos, ModelQuadView quad, QuadLight if (!Mth.equal(0f, x)) { final Direction face = x > 0 ? Direction.EAST : Direction.WEST; - final AoFaceData fd = gatherInsetFace(quad, blockPos, i, face, shade); + final AoFaceData fd = this.gatherInsetFace(quad, blockPos, i, face, shade); AoNeighborInfo.get(face).calculateCornerWeights(quad.getX(i), quad.getY(i), quad.getZ(i), w); final float n = x * x; - final float a = fd.getBlendedShade(w); + final float a = fd.getBlendedShade(w) * this.getAmbientBrightness(face, shade); final float s = fd.getBlendedSkyLight(w); final float b = fd.getBlendedBlockLight(w); ao += n * a; @@ -272,10 +279,10 @@ private void applyIrregularFace(BlockPos blockPos, ModelQuadView quad, QuadLight if (!Mth.equal(0f, y)) { final Direction face = y > 0 ? Direction.UP : Direction.DOWN; - final AoFaceData fd = gatherInsetFace(quad, blockPos, i, face, shade); + final AoFaceData fd = this.gatherInsetFace(quad, blockPos, i, face, shade); AoNeighborInfo.get(face).calculateCornerWeights(quad.getX(i), quad.getY(i), quad.getZ(i), w); final float n = y * y; - final float a = fd.getBlendedShade(w); + final float a = fd.getBlendedShade(w) * this.getAmbientBrightness(face, shade); final float s = fd.getBlendedSkyLight(w); final float b = fd.getBlendedBlockLight(w); ao += n * a; @@ -290,10 +297,10 @@ private void applyIrregularFace(BlockPos blockPos, ModelQuadView quad, QuadLight if (!Mth.equal(0f, z)) { final Direction face = z > 0 ? Direction.SOUTH : Direction.NORTH; - final AoFaceData fd = gatherInsetFace(quad, blockPos, i, face, shade); + final AoFaceData fd = this.gatherInsetFace(quad, blockPos, i, face, shade); AoNeighborInfo.get(face).calculateCornerWeights(quad.getX(i), quad.getY(i), quad.getZ(i), w); final float n = z * z; - final float a = fd.getBlendedShade(w); + final float a = fd.getBlendedShade(w) * this.getAmbientBrightness(face, shade); final float s = fd.getBlendedSkyLight(w); final float b = fd.getBlendedBlockLight(w); ao += n * a; @@ -309,29 +316,43 @@ private void applyIrregularFace(BlockPos blockPos, ModelQuadView quad, QuadLight } } - private void applySidedBrightness(AoFaceData out, Direction face, boolean shade) { - float brightness = this.lightCache.getLevel().getShade(face, shade); - float[] ao = out.ao; + /** + * Applies the "ambient" lighting from the dimension to a quad that is parallel with the block grid. + * @param brightness The array of brightnesses for each quad vertex + * @param face The facing of the quad + * @param shade Whether the quad should receive directional lighting + */ + private void applyAmbientLighting(final float[] brightness, Direction face, boolean shade) { + final float multiplier = this.getAmbientBrightness(face, shade); - for (int i = 0; i < ao.length; i++) { - ao[i] *= brightness; + for (int i = 0; i < brightness.length; i++) { + brightness[i] *= multiplier; } } + /** + * Returns the "ambient" brightness a block face receives in the world. + * @param face The block face + * @param shade Whether the block face is receiving directional light + */ + private float getAmbientBrightness(Direction face, boolean shade) { + return this.lightCache.getLevel() + .getShade(face, shade); + } + /** * Returns the cached data for a given facing or calculates it if it hasn't been cached. */ private AoFaceData getCachedFaceData(BlockPos pos, Direction face, boolean offset, boolean shade) { AoFaceData data = this.cachedFaceData[offset ? face.ordinal() : face.ordinal() + 6]; - if (!data.hasLightData()) { - data.initLightData(this.lightCache, pos, face, offset); - - this.applySidedBrightness(data, face, shade); - - data.unpackLightData(); + if (data.hasLightData()) { + return data; } + data.initLightData(this.lightCache, pos, face, offset); + data.unpackLightData(); + return data; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuad.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuad.java index d5f650313d..ef526b6a28 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuad.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuad.java @@ -125,7 +125,7 @@ public int getVertexNormal(int idx) { @Override public int getFaceNormal() { - return faceNormal; + return this.faceNormal; } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuadView.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuadView.java index 08369fb0af..49ebf030cb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuadView.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/ModelQuadView.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.model.quad; -import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFlags; import net.caffeinemc.mods.sodium.api.util.NormI8; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFlags; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.Direction; @@ -79,21 +79,21 @@ default boolean hasColor() { } default int calculateNormal() { - final float x0 = getX(0); - final float y0 = getY(0); - final float z0 = getZ(0); + final float x0 = this.getX(0); + final float y0 = this.getY(0); + final float z0 = this.getZ(0); - final float x1 = getX(1); - final float y1 = getY(1); - final float z1 = getZ(1); + final float x1 = this.getX(1); + final float y1 = this.getY(1); + final float z1 = this.getZ(1); - final float x2 = getX(2); - final float y2 = getY(2); - final float z2 = getZ(2); + final float x2 = this.getX(2); + final float y2 = this.getY(2); + final float z2 = this.getZ(2); - final float x3 = getX(3); - final float y3 = getY(3); - final float z3 = getZ(3); + final float x3 = this.getX(3); + final float y3 = this.getY(3); + final float z3 = this.getZ(3); final float dx0 = x2 - x0; final float dy0 = y2 - y0; @@ -123,8 +123,8 @@ default int calculateNormal() { * @return the per-vertex normal if it is set, otherwise the face normal. */ default int getAccurateNormal(int i) { - int normal = getVertexNormal(i); + int normal = this.getVertexNormal(i); - return normal == 0 ? getFaceNormal() : normal; + return normal == 0 ? this.getFaceNormal() : normal; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/blender/BlendedColorProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/blender/BlendedColorProvider.java index b1d53d164a..bf151359c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/blender/BlendedColorProvider.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/blender/BlendedColorProvider.java @@ -1,72 +1,62 @@ package net.caffeinemc.mods.sodium.client.model.quad.blender; -import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; +import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; import net.caffeinemc.mods.sodium.client.world.LevelSlice; -import net.caffeinemc.mods.sodium.api.util.ColorARGB; -import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.minecraft.core.BlockPos; import net.minecraft.util.Mth; public abstract class BlendedColorProvider implements ColorProvider { @Override - public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, T state, ModelQuadView quad, int[] output) { - for (int vertexIndex = 0; vertexIndex < 4; vertexIndex++) { - output[vertexIndex] = this.getVertexColor(slice, pos, scratchPos, quad, state, vertexIndex); + public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, T state, ModelQuadView quad, int[] output, boolean smooth) { + if (smooth) { + for (int vertexIndex = 0; vertexIndex < 4; vertexIndex++) { + output[vertexIndex] = this.getVertexColor(slice, pos, scratchPos, quad, state, vertexIndex); + } + } else { + int color = this.getColor(slice, state, pos); + for (int vertexIndex = 0; vertexIndex < 4; vertexIndex++) { + output[vertexIndex] = color; + } } } private int getVertexColor(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, ModelQuadView quad, T state, int vertexIndex) { - // Offset the position by -0.5f to align smooth blending with flat blending. - final float posX = quad.getX(vertexIndex) - 0.5f; - final float posY = quad.getY(vertexIndex) - 0.5f; - final float posZ = quad.getZ(vertexIndex) - 0.5f; - - // Floor the positions here to always get the largest integer below the input - // as negative values by default round toward zero when casting to an integer. - // Which would cause negative ratios to be calculated in the interpolation later on. - final int posIntX = Mth.floor(posX); - final int posIntY = Mth.floor(posY); - final int posIntZ = Mth.floor(posZ); - - // Integer component of position vector - final int blockIntX = pos.getX() + posIntX; - final int blockIntY = pos.getY() + posIntY; - final int blockIntZ = pos.getZ() + posIntZ; - - // Retrieve the color values for each neighboring block - final int c00 = this.getColor(slice, state, scratchPos.set(blockIntX + 0, blockIntY, blockIntZ + 0)); - final int c01 = this.getColor(slice, state, scratchPos.set(blockIntX + 0, blockIntY, blockIntZ + 1)); - final int c10 = this.getColor(slice, state, scratchPos.set(blockIntX + 1, blockIntY, blockIntZ + 0)); - final int c11 = this.getColor(slice, state, scratchPos.set(blockIntX + 1, blockIntY, blockIntZ + 1)); - - // Linear interpolation across the Z-axis - int z0; - - if (c00 != c01) { - z0 = ColorMixer.mix(c00, c01, posZ - posIntZ); - } else { - z0 = c00; - } - - int z1; - - if (c10 != c11) { - z1 = ColorMixer.mix(c10, c11, posZ - posIntZ); - } else { - z1 = c10; - } - - // Linear interpolation across the X-axis - int x0; - - if (z0 != z1) { - x0 = ColorMixer.mix(z0, z1, posX - posIntX); - } else { - x0 = z0; - } - - return x0; + // The vertex position + // We add a half-texel offset since we are sampling points within a color texture + final float x = quad.getX(vertexIndex) - 0.5f; + final float y = quad.getY(vertexIndex) - 0.5f; + final float z = quad.getZ(vertexIndex) - 0.5f; + + // Integer component of vertex position + final int intX = Mth.floor(x); + final int intY = Mth.floor(y); + final int intZ = Mth.floor(z); + + // Fractional component of vertex position + final float fracX = x - intX; + final float fracY = y - intY; + final float fracZ = z - intZ; + + // Block coordinates (in world space) which the vertex is located within + // This is calculated after converting from floating point to avoid precision loss with large coordinates + final int blockX = pos.getX() + intX; + final int blockY = pos.getY() + intY; + final int blockZ = pos.getZ() + intZ; + + // Retrieve the color values for each neighboring value + // This creates a 2x2 matrix which is then sampled during interpolation + final int m00 = this.getColor(slice, state, scratchPos.set(blockX + 0, blockY, blockZ + 0)); + final int m01 = this.getColor(slice, state, scratchPos.set(blockX + 0, blockY, blockZ + 1)); + final int m10 = this.getColor(slice, state, scratchPos.set(blockX + 1, blockY, blockZ + 0)); + final int m11 = this.getColor(slice, state, scratchPos.set(blockX + 1, blockY, blockZ + 1)); + + // Perform interpolation across the X-axis, and then Y-axis + // y0 = (m00 * (1.0 - x)) + (m10 * x) + // y1 = (m01 * (1.0 - x)) + (m11 * x) + // result = (y0 * (1.0 - y)) + (y1 * y) + return ColorMixer.mix2d(m00, m01, m10, m11, fracX, fracZ); } protected abstract int getColor(LevelSlice slice, T state, BlockPos pos); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/properties/ModelQuadFacing.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/properties/ModelQuadFacing.java index 7c357054f7..9fbdffbb75 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/properties/ModelQuadFacing.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/model/quad/properties/ModelQuadFacing.java @@ -1,10 +1,9 @@ package net.caffeinemc.mods.sodium.client.model.quad.properties; -import net.caffeinemc.mods.sodium.client.util.DirectionUtil; import net.caffeinemc.mods.sodium.api.util.NormI8; +import net.caffeinemc.mods.sodium.client.util.DirectionUtil; import net.minecraft.core.Direction; import net.minecraft.util.Mth; -import org.joml.Math; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -23,6 +22,7 @@ public enum ModelQuadFacing { public static final int COUNT = VALUES.length; public static final int DIRECTIONS = VALUES.length - 1; + public static final int UNASSIGNED_ORDINAL = ModelQuadFacing.UNASSIGNED.ordinal(); public static final int NONE = 0; public static final int ALL = (1 << COUNT) - 1; @@ -103,14 +103,22 @@ public int getPackedAlignedNormal() { return PACKED_ALIGNED_NORMALS[this.ordinal()]; } - public static ModelQuadFacing fromNormal(float x, float y, float z) { - if (!(Math.isFinite(x) && Math.isFinite(y) && Math.isFinite(z))) { - return ModelQuadFacing.UNASSIGNED; + public static ModelQuadFacing fromNormal(Vector3fc normal) { + for (Direction face : DirectionUtil.ALL_DIRECTIONS) { + var step = face.step(); + if (step.equals(normal, Mth.EPSILON)) { + return ModelQuadFacing.fromDirection(face); + } } + return ModelQuadFacing.UNASSIGNED; + } + + + public static ModelQuadFacing fromNormal(float x, float y, float z) { for (Direction face : DirectionUtil.ALL_DIRECTIONS) { var step = face.step(); - if (Mth.equal(Math.fma(x, step.x(), Math.fma(y, step.y(), z * step.z())), 1.0f)) { + if (Mth.equal(x, step.x()) && Mth.equal(y, step.y()) && Mth.equal(z, step.z())) { return ModelQuadFacing.fromDirection(face); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 428b7eed1c..527296e79d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -17,9 +17,11 @@ import net.caffeinemc.mods.sodium.client.render.chunk.map.ChunkTracker; import net.caffeinemc.mods.sodium.client.render.chunk.map.ChunkTrackerHolder; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; +import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; import net.caffeinemc.mods.sodium.client.world.LevelRendererExtension; import net.minecraft.client.Camera; @@ -189,14 +191,14 @@ public void setupTerrain(Camera camera, float fogDistance = RenderSystem.getShaderFogEnd(); if (this.lastCameraPos == null) { - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } if (this.lastProjectionMatrix == null) { this.lastProjectionMatrix = new Matrix4f(projectionMatrix); } boolean cameraLocationChanged = !pos.equals(this.lastCameraPos); boolean cameraAngleChanged = pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance; - boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix); + boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix, 0.0001f); this.lastProjectionMatrix = projectionMatrix; @@ -209,13 +211,13 @@ public void setupTerrain(Camera camera, this.lastFogDistance = fogDistance; - this.renderSectionManager.updateCameraState(pos, camera); + this.renderSectionManager.prepareFrame(pos); if (cameraLocationChanged) { profiler.popPush("translucent_triggering"); this.renderSectionManager.processGFNIMovement(new CameraMovement(this.lastCameraPos, pos)); - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } int maxChunkUpdates = updateChunksImmediately ? this.renderDistance : 1; @@ -241,6 +243,10 @@ public void setupTerrain(Camera camera, } } + profiler.popPush("chunk_render_lists"); + + this.renderSectionManager.finalizeRenderLists(viewport); + profiler.popPush("chunk_render_tick"); this.renderSectionManager.tickVisibleRenders(); @@ -251,6 +257,7 @@ public void setupTerrain(Camera camera, } private void processChunkEvents() { + this.renderSectionManager.beforeSectionUpdates(); var tracker = ChunkTrackerHolder.get(this.level); tracker.forEachEvent(this.renderSectionManager::onChunkAdded, this.renderSectionManager::onChunkRemoved); } @@ -283,9 +290,17 @@ private void initRenderer(CommandList commandList) { this.renderSectionManager = null; } + // translucency sorting can be disabled in development environments by setting the debug option in the config file + var sortBehavior = SortBehavior.DYNAMIC_DEFER_NEARBY_ZERO_FRAMES; + + if (PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment() + && !SodiumClientMod.options().debug.terrainSortingEnabled) { + sortBehavior = SortBehavior.OFF; + } + this.renderDistance = this.client.options.getEffectiveRenderDistance(); - this.renderSectionManager = new RenderSectionManager(this.level, this.renderDistance, commandList); + this.renderSectionManager = new RenderSectionManager(this.level, this.renderDistance, sortBehavior, commandList); var tracker = ChunkTrackerHolder.get(this.level); ChunkTracker.forEachChunk(tracker.getReadyChunks(), this.renderSectionManager::onChunkAdded); @@ -473,7 +488,7 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 15; /** - * Returns whether or not the entity intersects with any visible chunks in the graph. + * Returns whether the entity intersects with any visible chunks in the graph. * @return True if the entity is visible, otherwise false */ public boolean isEntityVisible(Entity entity) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/BlockEntityRenderHandlerImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/BlockEntityRenderHandlerImpl.java index 56f323d708..e1809eabcb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/BlockEntityRenderHandlerImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/BlockEntityRenderHandlerImpl.java @@ -1,7 +1,5 @@ package net.caffeinemc.mods.sodium.client.render.chunk; -import java.util.function.Predicate; - import net.caffeinemc.mods.sodium.api.blockentity.BlockEntityRenderHandler; import net.caffeinemc.mods.sodium.api.blockentity.BlockEntityRenderPredicate; import net.minecraft.world.level.block.entity.BlockEntity; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkRenderer.java index 2db8a0bc9c..9db3ac0d85 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkRenderer.java @@ -13,13 +13,14 @@ public interface ChunkRenderer { /** * Renders the given chunk render list to the active framebuffer. * - * @param matrices The camera matrices to use for rendering - * @param commandList The command list which OpenGL commands should be serialized to - * @param renderLists The collection of render lists - * @param pass The block render pass to execute - * @param camera The camera context containing chunk offsets for the current render + * @param matrices The camera matrices to use for rendering + * @param commandList The command list which OpenGL commands should be serialized to + * @param renderLists The collection of render lists + * @param pass The block render pass to execute + * @param camera The camera context containing chunk offsets for the current render + * @param indexedRenderingEnabled Whether indexed rendering is enabled */ - void render(ChunkRenderMatrices matrices, CommandList commandList, ChunkRenderListIterable renderLists, TerrainRenderPass pass, CameraTransform camera); + void render(ChunkRenderMatrices matrices, CommandList commandList, ChunkRenderListIterable renderLists, TerrainRenderPass pass, CameraTransform camera, boolean indexedRenderingEnabled); /** * Deletes this render backend and any resources attached to it. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java deleted file mode 100644 index 2cd7859716..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk; - -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; - -public enum ChunkUpdateType { - SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT), - INITIAL_BUILD(128, ChunkBuilder.HIGH_EFFORT), - REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT); - - private final int maximumQueueSize; - private final int taskEffort; - - ChunkUpdateType(int maximumQueueSize, int taskEffort) { - this.maximumQueueSize = maximumQueueSize; - this.taskEffort = taskEffort; - } - - public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, ChunkUpdateType next) { - if (prev == null || prev == SORT || prev == next) { - return next; - } - if (next == IMPORTANT_REBUILD - || (prev == IMPORTANT_SORT && next == REBUILD) - || (prev == REBUILD && next == IMPORTANT_SORT)) { - return IMPORTANT_REBUILD; - } - return null; - } - - public int getMaximumQueueSize() { - return this.maximumQueueSize; - } - - public boolean isImportant() { - return this == IMPORTANT_REBUILD || this == IMPORTANT_SORT; - } - - public int getTaskEffort() { - return this.taskEffort; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateTypes.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateTypes.java new file mode 100644 index 0000000000..b7ae6df533 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateTypes.java @@ -0,0 +1,54 @@ +package net.caffeinemc.mods.sodium.client.render.chunk; + +/** + * Important: Whether the task is scheduled immediately after its creation. Otherwise, they're scheduled through + * asynchronous culling that collects non-important tasks. Defer mode: For important tasks, how fast they are going to + * be executed. One or zero frame deferral only allows one or zero frames to pass before the frame blocks on the task. + * Always deferral allows the task to be deferred indefinitely, but if it's important it will still be put to the front + * of the queue. + */ +public class ChunkUpdateTypes { + public static final int SORT = 0b001; + public static final int REBUILD = 0b010; + public static final int IMPORTANT = 0b100; + public static final int INITIAL_BUILD = 0b1000; + + public static int join(int from, int to) { + return from | to; + } + + public static boolean isSort(int type) { + return (type & SORT) != 0; + } + + public static boolean isRebuild(int type) { + return (type & REBUILD) != 0; + } + + public static boolean isImportant(int type) { + return (type & IMPORTANT) != 0; + } + + public static boolean isInitialBuild(int type) { + return (type & INITIAL_BUILD) != 0; + } + + public static boolean isRebuildWithSort(int type) { + return (isRebuild(type) || isInitialBuild(type)) && isSort(type); + } + + public static TaskQueueType getQueueType(int type, TaskQueueType importantRebuildQueueType, TaskQueueType importantSortQueueType) { + if (isInitialBuild(type)) { + return TaskQueueType.INITIAL_BUILD; + } + if (isImportant(type)) { + if (isRebuild(type)) { + return importantRebuildQueueType; + } else { // implies important sort task + return importantSortQueueType; + } + } else { + return TaskQueueType.ALWAYS_DEFER; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DefaultChunkRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DefaultChunkRenderer.java index e273e61744..55ac9c9273 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DefaultChunkRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DefaultChunkRenderer.java @@ -1,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import net.caffeinemc.mods.sodium.client.SodiumClientMod; -import net.caffeinemc.mods.sodium.client.gl.attribute.GlVertexAttributeBinding; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.DrawCommandList; import net.caffeinemc.mods.sodium.client.gl.device.MultiDrawBatch; @@ -16,27 +15,23 @@ import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderListIterable; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; -import net.caffeinemc.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; import net.caffeinemc.mods.sodium.client.render.chunk.shader.ChunkShaderInterface; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.util.BitwiseMath; import net.caffeinemc.mods.sodium.client.util.UInt32; import org.lwjgl.system.MemoryUtil; +import org.lwjgl.system.Pointer; import java.util.Iterator; public class DefaultChunkRenderer extends ShaderChunkRenderer { - private final MultiDrawBatch batch; - private final SharedQuadIndexBuffer sharedIndexBuffer; public DefaultChunkRenderer(RenderDevice device, ChunkVertexType vertexType) { super(device, vertexType); - this.batch = new MultiDrawBatch((ModelQuadFacing.COUNT * RenderRegion.REGION_SIZE) + 1); this.sharedIndexBuffer = new SharedQuadIndexBuffer(device.createCommandList(), SharedQuadIndexBuffer.IndexType.INTEGER); } @@ -50,11 +45,12 @@ public void render(ChunkRenderMatrices matrices, CommandList commandList, ChunkRenderListIterable renderLists, TerrainRenderPass renderPass, - CameraTransform camera) { + CameraTransform camera, + boolean indexedRenderingEnabled) { super.begin(renderPass); final boolean useBlockFaceCulling = SodiumClientMod.options().performance.useBlockFaceCulling; - final boolean useIndexedTessellation = isTranslucentRenderPass(renderPass); + final boolean useIndexedTessellation = renderPass.isTranslucent() && indexedRenderingEnabled; ChunkShaderInterface shader = this.activeProgram.getInterface(); shader.setProjectionMatrix(matrices.projection()); @@ -72,45 +68,51 @@ public void render(ChunkRenderMatrices matrices, continue; } - fillCommandBuffer(this.batch, region, storage, renderList, camera, renderPass, useBlockFaceCulling); + var resources = region.getResources(); + if (resources == null) { + region.clearCachedBatchFor(renderPass); + continue; + } + + var batch = region.getCachedBatch(renderPass); + if (!batch.isFilled) { + fillCommandBuffer(batch, region, storage, renderList, camera, renderPass, useBlockFaceCulling, useIndexedTessellation); + } - if (this.batch.isEmpty()) { + if (batch.isEmpty()) { continue; } // When the shared index buffer is being used, we must ensure the storage has been allocated *before* // the tessellation is prepared. if (!useIndexedTessellation) { - this.sharedIndexBuffer.ensureCapacity(commandList, this.batch.getIndexBufferSize()); + this.sharedIndexBuffer.ensureCapacity(commandList, batch.getIndexBufferSize()); } GlTessellation tessellation; if (useIndexedTessellation) { - tessellation = this.prepareIndexedTessellation(commandList, region); + tessellation = this.prepareIndexedTessellation(commandList, resources); } else { - tessellation = this.prepareTessellation(commandList, region); + tessellation = this.prepareTessellation(commandList, resources); } setModelMatrixUniforms(shader, region, camera); - executeDrawBatch(commandList, tessellation, this.batch); + executeDrawBatch(commandList, tessellation, batch); } super.end(renderPass); } - private static boolean isTranslucentRenderPass(TerrainRenderPass renderPass) { - return renderPass.isTranslucent() && SodiumClientMod.options().performance.getSortBehavior() != SortBehavior.OFF; - } - private static void fillCommandBuffer(MultiDrawBatch batch, RenderRegion renderRegion, SectionRenderDataStorage renderDataStorage, ChunkRenderList renderList, CameraTransform camera, TerrainRenderPass pass, - boolean useBlockFaceCulling) { - batch.clear(); + boolean useBlockFaceCulling, + boolean useIndexedTessellation) { + batch.isFilled = true; var iterator = renderList.sectionsWithGeometryIterator(pass.isTranslucent()); @@ -149,30 +151,48 @@ private static void fillCommandBuffer(MultiDrawBatch batch, continue; } - if (pass.isTranslucent()) { - addIndexedDrawCommands(batch, pMeshData, slices); + // it's necessary to sometimes not the locally-indexed command generator even for indexed tessellations since + // sometimes the index buffer is shared, but not globally shared. This means that translucent sections that + // are sharing an index buffer amongst them need to use the shared index command generator since it sets the + // same element offset for each draw command and doesn't increment it. Recall that in each draw command the indexing + // of the elements needs to start at 0 and thus starting somewhere further into the shared index buffer is invalid. + // there's also the optimization that draw commands can be combined when using a shared index buffer, be it + // globally shared or just shared within the region, which isn't possible with the locally-indexed command generator. + if (useIndexedTessellation && SectionRenderDataUnsafe.isLocalIndex(pMeshData)) { + addLocalIndexedDrawCommands(batch, pMeshData, slices); } else { - addNonIndexedDrawCommands(batch, pMeshData, slices); + addSharedIndexedDrawCommands(batch, pMeshData, slices); } } } /** - * Generates the draw commands for a chunk's meshes using the shared index buffer. + * Generates the draw commands for a chunk's meshes, where each mesh has a separate index buffer. This is used + * when rendering translucent geometry, as each geometry set needs a sorted index buffer. */ @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") - private static void addNonIndexedDrawCommands(MultiDrawBatch batch, long pMeshData, int mask) { + private static void addLocalIndexedDrawCommands(MultiDrawBatch batch, long pMeshData, int mask) { final var pElementPointer = batch.pElementPointer; final var pBaseVertex = batch.pBaseVertex; final var pElementCount = batch.pElementCount; int size = batch.size; + long elementOffset = SectionRenderDataUnsafe.getBaseElement(pMeshData); + long baseVertex = SectionRenderDataUnsafe.getBaseVertex(pMeshData); + for (int facing = 0; facing < ModelQuadFacing.COUNT; facing++) { - // Uint32 -> Int32 cast is always safe and should be optimized away - MemoryUtil.memPutInt(pBaseVertex + (size << 2), (int) SectionRenderDataUnsafe.getVertexOffset(pMeshData, facing)); - MemoryUtil.memPutInt(pElementCount + (size << 2), (int) SectionRenderDataUnsafe.getElementCount(pMeshData, facing)); - MemoryUtil.memPutAddress(pElementPointer + (size << 3), 0 /* using a shared index buffer */); + final long vertexCount = SectionRenderDataUnsafe.getVertexCount(pMeshData, facing); + final long elementCount = (vertexCount >> 2) * 6; + + MemoryUtil.memPutInt(pElementCount + (size << 2), UInt32.uncheckedDowncast(elementCount)); + MemoryUtil.memPutInt(pBaseVertex + (size << 2), UInt32.uncheckedDowncast(baseVertex)); + + // * 4 to convert to bytes (the index buffer contains integers) + MemoryUtil.memPutAddress(pElementPointer + (size << Pointer.POINTER_SHIFT), elementOffset << 2); + + baseVertex += vertexCount; + elementOffset += elementCount; size += (mask >> facing) & 1; } @@ -181,34 +201,57 @@ private static void addNonIndexedDrawCommands(MultiDrawBatch batch, long pMeshDa } /** - * Generates the draw commands for a chunk's meshes, where each mesh has a separate index buffer. This is used - * when rendering translucent geometry, as each geometry set needs a sorted index buffer. + * Generates the draw commands for a chunk's meshes using the shared index buffer. */ @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") - private static void addIndexedDrawCommands(MultiDrawBatch batch, long pMeshData, int mask) { + private static void addSharedIndexedDrawCommands(MultiDrawBatch batch, long pMeshData, int mask) { final var pElementPointer = batch.pElementPointer; final var pBaseVertex = batch.pBaseVertex; final var pElementCount = batch.pElementCount; - int size = batch.size; + // this is either zero (global shared index buffer) or the offset to the location of the shared element buffer (region shared index buffer) + final var elementOffsetBytes = SectionRenderDataUnsafe.getBaseElement(pMeshData) << 2; + final var facingList = SectionRenderDataUnsafe.getFacingList(pMeshData); - long elementOffset = SectionRenderDataUnsafe.getBaseElement(pMeshData); - - for (int facing = 0; facing < ModelQuadFacing.COUNT; facing++) { - final long vertexOffset = SectionRenderDataUnsafe.getVertexOffset(pMeshData, facing); - final long elementCount = SectionRenderDataUnsafe.getElementCount(pMeshData, facing); - - // Uint32 -> Int32 cast is always safe and should be optimized away - MemoryUtil.memPutInt(pBaseVertex + (size << 2), UInt32.uncheckedDowncast(vertexOffset)); - MemoryUtil.memPutInt(pElementCount + (size << 2), UInt32.uncheckedDowncast(elementCount)); + int size = batch.size; + long groupVertexCount = 0; + long baseVertex = SectionRenderDataUnsafe.getBaseVertex(pMeshData); + int lastMaskBit = 0; + + for (int i = 0; i <= ModelQuadFacing.COUNT; i++) { + var maskBit = 0; + long vertexCount = 0; + if (i < ModelQuadFacing.COUNT) { + vertexCount = SectionRenderDataUnsafe.getVertexCount(pMeshData, i); + + // if there's no vertexes, the mask bit is just 0 + if (vertexCount != 0) { + var facing = (facingList >>> (i * 8)) & 0xFF; + maskBit = (mask >>> facing) & 1; + } + } - // * 4 to convert to bytes (the index buffer contains integers) - // the section render data storage for the indices stores the offset in indices (also called elements) - MemoryUtil.memPutAddress(pElementPointer + (size << 3), elementOffset << 2); + if (maskBit == 0) { + if (lastMaskBit == 1) { + // delay writing out draw command if there's a zero-size group + if (i < ModelQuadFacing.COUNT && vertexCount == 0) { + continue; + } + + MemoryUtil.memPutInt(pElementCount + (size << 2), UInt32.uncheckedDowncast((groupVertexCount >> 2) * 6)); + MemoryUtil.memPutInt(pBaseVertex + (size << 2), UInt32.uncheckedDowncast(baseVertex)); + MemoryUtil.memPutAddress(pElementPointer + (size << Pointer.POINTER_SHIFT), elementOffsetBytes); + size++; + baseVertex += groupVertexCount; + groupVertexCount = 0; + } + + baseVertex += vertexCount; + } else { + groupVertexCount += vertexCount; + } - // adding the number of elements works because the index data has one index per element (which are the indices) - elementOffset += elementCount; - size += (mask >> facing) & 1; + lastMaskBit = maskBit; } batch.size = size; @@ -223,7 +266,7 @@ private static void addIndexedDrawCommands(MultiDrawBatch batch, long pMeshData, private static final int MODEL_NEG_Y = ModelQuadFacing.NEG_Y.ordinal(); private static final int MODEL_NEG_Z = ModelQuadFacing.NEG_Z.ordinal(); - private static int getVisibleFaces(int originX, int originY, int originZ, int chunkX, int chunkY, int chunkZ) { + public static int getVisibleFaces(int originX, int originY, int originZ, int chunkX, int chunkY, int chunkZ) { // This is carefully written so that we can keep everything branch-less. // // Normally, this would be a ridiculous way to handle the problem. But the Hotspot VM's @@ -277,9 +320,7 @@ private static float getCameraTranslation(int chunkBlockPos, int cameraBlockPos, return (chunkBlockPos - cameraBlockPos) - cameraPos; } - private GlTessellation prepareTessellation(CommandList commandList, RenderRegion region) { - var resources = region.getResources(); - + private GlTessellation prepareTessellation(CommandList commandList, RenderRegion.DeviceResources resources) { GlTessellation tessellation = resources.getTessellation(); if (tessellation == null) { tessellation = this.createRegionTessellation(commandList, resources, true); @@ -289,9 +330,7 @@ private GlTessellation prepareTessellation(CommandList commandList, RenderRegion return tessellation; } - private GlTessellation prepareIndexedTessellation(CommandList commandList, RenderRegion region) { - var resources = region.getResources(); - + private GlTessellation prepareIndexedTessellation(CommandList commandList, RenderRegion.DeviceResources resources) { GlTessellation tessellation = resources.getIndexedTessellation(); if (tessellation == null) { tessellation = this.createRegionTessellation(commandList, resources, false); @@ -321,6 +360,5 @@ public void delete(CommandList commandList) { super.delete(commandList); this.sharedIndexBuffer.delete(commandList); - this.batch.delete(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java new file mode 100644 index 0000000000..608ddcf666 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.sodium.client.render.chunk; + +import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.minecraft.network.chat.Component; + +public enum DeferMode implements TextProvider { + ALWAYS("sodium.options.defer_chunk_updates.always", TaskQueueType.ALWAYS_DEFER), + ONE_FRAME("sodium.options.defer_chunk_updates.one_frame", TaskQueueType.ONE_FRAME_DEFER), + ZERO_FRAMES("sodium.options.defer_chunk_updates.zero_frames", TaskQueueType.ZERO_FRAME_DEFER); + + private final Component name; + private final TaskQueueType importantRebuildQueueType; + + DeferMode(String name, TaskQueueType importantRebuildQueueType) { + this.name = Component.translatable(name); + this.importantRebuildQueueType = importantRebuildQueueType; + } + + @Override + public Component getLocalizedName() { + return this.name; + } + + public TaskQueueType getImportantRebuildQueueType() { + return this.importantRebuildQueueType; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ExtendedBlockEntityType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ExtendedBlockEntityType.java index c74e5712a6..bca68c35da 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ExtendedBlockEntityType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ExtendedBlockEntityType.java @@ -6,8 +6,6 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; -import java.util.function.Predicate; - @SuppressWarnings("unchecked") public interface ExtendedBlockEntityType { BlockEntityRenderPredicate[] sodium$getRenderPredicates(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 0103850155..c16c371249 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -1,12 +1,13 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJob; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirectionSet; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.VisibilityEncoding; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; -import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; @@ -53,10 +54,11 @@ public class RenderSection { // Pending Update State @Nullable - private CancellationToken taskCancellationToken = null; + private ChunkJob runningJob = null; + private long lastMeshResultSize = MeshResultSize.NO_DATA; - @Nullable - private ChunkUpdateType pendingUpdateType; + private int pendingUpdateType; + private long pendingUpdateSince; private int lastUploadFrame = -1; private int lastSubmittedFrame = -1; @@ -69,9 +71,9 @@ public RenderSection(RenderRegion region, int chunkX, int chunkY, int chunkZ) { this.chunkY = chunkY; this.chunkZ = chunkZ; - int rX = this.getChunkX() & (RenderRegion.REGION_WIDTH - 1); - int rY = this.getChunkY() & (RenderRegion.REGION_HEIGHT - 1); - int rZ = this.getChunkZ() & (RenderRegion.REGION_LENGTH - 1); + int rX = this.getChunkX() & RenderRegion.REGION_WIDTH_M; + int rY = this.getChunkY() & RenderRegion.REGION_HEIGHT_M; + int rZ = this.getChunkZ() & RenderRegion.REGION_LENGTH_M; this.sectionIndex = LocalSectionIndex.pack(rX, rY, rZ); @@ -130,39 +132,61 @@ public void setTranslucentData(TranslucentData translucentData) { * be used. */ public void delete() { - if (this.taskCancellationToken != null) { - this.taskCancellationToken.setCancelled(); - this.taskCancellationToken = null; + if (this.runningJob != null) { + this.runningJob.setCancelled(); + this.runningJob = null; } this.clearRenderState(); this.disposed = true; } - public void setInfo(@Nullable BuiltSectionInfo info) { + public boolean setInfo(@Nullable BuiltSectionInfo info) { if (info != null) { - this.setRenderState(info); + return this.setRenderState(info); } else { - this.clearRenderState(); + return this.clearRenderState(); } } - private void setRenderState(@NotNull BuiltSectionInfo info) { + private boolean setRenderState(@NotNull BuiltSectionInfo info) { + var prevBuilt = this.built; + var prevFlags = this.flags; + var prevVisibilityData = this.visibilityData; + this.built = true; this.flags = info.flags; this.visibilityData = info.visibilityData; + this.globalBlockEntities = info.globalBlockEntities; this.culledBlockEntities = info.culledBlockEntities; this.animatedSprites = info.animatedSprites; + + // the section is marked as having received graph-relevant changes if it's build state, flags, or connectedness has changed. + // the entities and sprites don't need to be checked since whether they exist is encoded in the flags. + return !prevBuilt || prevFlags != this.flags || prevVisibilityData != this.visibilityData; } - private void clearRenderState() { + private boolean clearRenderState() { + var wasBuilt = this.built; + this.built = false; this.flags = RenderSectionFlags.NONE; this.visibilityData = VisibilityEncoding.NULL; this.globalBlockEntities = null; this.culledBlockEntities = null; this.animatedSprites = null; + + // changes to data if it moves from built to not built don't matter, so only build state changes matter + return wasBuilt; + } + + public void setLastMeshResultSize(long size) { + this.lastMeshResultSize = size; + } + + public long getLastMeshResultSize() { + return this.lastMeshResultSize; } /** @@ -325,20 +349,29 @@ public long getVisibilityData() { return this.globalBlockEntities; } - public @Nullable CancellationToken getTaskCancellationToken() { - return this.taskCancellationToken; + public @Nullable ChunkJob getRunningJob() { + return this.runningJob; } - public void setTaskCancellationToken(@Nullable CancellationToken token) { - this.taskCancellationToken = token; + public void setRunningJob(@Nullable ChunkJob token) { + this.runningJob = token; } - public @Nullable ChunkUpdateType getPendingUpdate() { + public int getPendingUpdate() { return this.pendingUpdateType; } - public void setPendingUpdate(@Nullable ChunkUpdateType type) { + public long getPendingUpdateSince() { + return this.pendingUpdateSince; + } + + public void setPendingUpdate(int type, long now) { this.pendingUpdateType = type; + this.pendingUpdateSince = now; + } + + public void clearPendingUpdate() { + this.pendingUpdateType = 0; } public void prepareTrigger(boolean isDirectTrigger) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index cb19504069..904cae0364 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -5,5 +5,18 @@ public class RenderSectionFlags { public static final int HAS_BLOCK_ENTITIES = 1; public static final int HAS_ANIMATED_SPRITES = 2; + public static final int MASK_HAS_BLOCK_GEOMETRY = 1 << HAS_BLOCK_GEOMETRY; + public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; + public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; + public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; + public static final int NONE = 0; + + public static boolean needsRender(int flags) { + return (flags & MASK_NEEDS_RENDER) != 0; + } + + public static int getNewRenderFlags(int prevFlags, int newFlags) { + return ((newFlags & MASK_NEEDS_RENDER) & ~(prevFlags & MASK_NEEDS_RENDER)); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 21aae7a7e1..03b3c458d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -4,43 +4,41 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -import it.unimi.dsi.fastutil.objects.ReferenceSet; -import it.unimi.dsi.fastutil.objects.ReferenceSets; +import it.unimi.dsi.fastutil.objects.*; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.*; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.*; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.DeferMode; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.PriorityMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.NoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.RemovableMultiForest; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.util.RenderAsserts; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.world.LevelSlice; import net.caffeinemc.mods.sodium.client.world.cloned.ChunkRenderContext; @@ -63,6 +61,13 @@ import java.util.concurrent.ConcurrentLinkedDeque; public class RenderSectionManager { + private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); + private static final float IMMEDIATE_PRESENT_DISTANCE = Mth.square(64.0f); + private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); + + private static final float FRAME_DURATION_UPLOAD_FRACTION = 0.1f; + private static final long MIN_UPLOAD_DURATION_BUDGET = 2_000_000L; // 2ms + private final ChunkBuilder builder; private final RenderRegionManager regions; @@ -71,6 +76,13 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); + private final JobDurationEstimator jobDurationEstimator = new JobDurationEstimator(); + private final MeshTaskSizeEstimator meshTaskSizeEstimator; + private final UploadDurationEstimator jobUploadDurationEstimator = new UploadDurationEstimator(); + private ChunkJobCollector lastBlockingCollector; + private int thisFrameBlockingTasks; + private int nextFrameBlockingTasks; + private int deferredTasks; private final ChunkRenderer chunkRenderer; @@ -81,34 +93,47 @@ public class RenderSectionManager { private final OcclusionCuller occlusionCuller; private final int renderDistance; + private final SortBehavior sortBehavior; private final SortTriggering sortTriggering; - private ChunkJobCollector lastBlockingCollector; - @NotNull private SortedRenderLists renderLists; + private SectionCollector sectionCollector; + private SectionCollector lastSectionCollector; @NotNull - private Map> taskLists; + private Map> taskLists; - private int lastUpdatedFrame; + private int frame; + private long lastFrameDuration = -1; + private long averageFrameDuration = -1; + private long lastFrameAtTime = System.nanoTime(); + private static final float FRAME_DURATION_UPDATE_RATIO = 0.05f; - private boolean needsGraphUpdate; + private boolean needsGraphUpdate = true; + private int lastUpdatedFrame; - private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; - public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) { + private final RemovableMultiForest renderableSectionTree; + + public RenderSectionManager(ClientLevel level, int renderDistance, SortBehavior sortBehavior, CommandList commandList) { + this.meshTaskSizeEstimator = new MeshTaskSizeEstimator(level); + this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT); this.level = level; this.builder = new ChunkBuilder(level, ChunkMeshFormats.COMPACT); - this.needsGraphUpdate = true; this.renderDistance = renderDistance; + this.sortBehavior = sortBehavior; - this.sortTriggering = new SortTriggering(); + if (this.sortBehavior != SortBehavior.OFF) { + this.sortTriggering = new SortTriggering(); + } else { + this.sortTriggering = null; + } this.regions = new RenderRegionManager(commandList); this.sectionCache = new ClonedChunkSectionCache(this.level); @@ -116,38 +141,81 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.renderLists = SortedRenderLists.empty(); this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level); - this.taskLists = new EnumMap<>(ChunkUpdateType.class); + this.renderableSectionTree = new RemovableMultiForest(renderDistance); - for (var type : ChunkUpdateType.values()) { + this.taskLists = new EnumMap<>(TaskQueueType.class); + + for (var type : TaskQueueType.values()) { this.taskLists.put(type, new ArrayDeque<>()); } } - public void updateCameraState(Vector3dc cameraPosition, Camera camera) { - this.cameraBlockPos = camera.getBlockPosition(); + public void prepareFrame(Vector3dc cameraPosition) { + var now = System.nanoTime(); + this.lastFrameDuration = now - this.lastFrameAtTime; + this.lastFrameAtTime = now; + if (this.averageFrameDuration == -1) { + this.averageFrameDuration = this.lastFrameDuration; + } else { + this.averageFrameDuration = MathUtil.exponentialMovingAverage(this.averageFrameDuration, this.lastFrameDuration, FRAME_DURATION_UPDATE_RATIO); + } + this.averageFrameDuration = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000); + + this.frame += 1; + this.cameraPosition = cameraPosition; } public void update(Camera camera, Viewport viewport, boolean spectator) { this.lastUpdatedFrame += 1; - this.createTerrainRenderList(camera, viewport, this.lastUpdatedFrame, spectator); - - this.needsGraphUpdate = false; + this.needsGraphUpdate = this.createTerrainRenderList(camera, viewport, this.lastUpdatedFrame, spectator); } - private void createTerrainRenderList(Camera camera, Viewport viewport, int frame, boolean spectator) { + private boolean createTerrainRenderList(Camera camera, Viewport viewport, int frame, boolean spectator) { this.resetRenderLists(); final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - var visitor = new VisibleChunkCollector(frame); + var importantRebuildQueueType = SodiumClientMod.options().performance.chunkBuildDeferMode.getImportantRebuildQueueType(); + var importantSortQueueType = this.sortBehavior.getDeferMode().getImportantRebuildQueueType(); + if (this.isOutOfGraph(viewport.getChunkCoord())) { + var visitor = new TreeSectionCollector(frame, importantRebuildQueueType, importantSortQueueType, this.sectionByPosition); + this.renderableSectionTree.prepareForTraversal(); + this.renderableSectionTree.traverse(visitor, viewport, searchDistance); + + this.sectionCollector = visitor; + } else { + var visitor = new OcclusionSectionCollector(frame, importantRebuildQueueType, importantSortQueueType); + this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame); + + this.sectionCollector = visitor; + } + this.lastSectionCollector = null; - this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame); + this.taskLists = this.sectionCollector.getTaskLists(); - this.renderLists = visitor.createRenderLists(); - this.taskLists = visitor.getRebuildLists(); + // when there were sections with pending updates that were skipped because they already had a task running, + // it needs to revisit them to schedule the remaining pending updates. + // since not all tasks necessarily change the section info to trigger a graph update, + // without this pending updates might be missed when the camera is stationary + return this.sectionCollector.needsRevisitForPendingUpdates(); + } + + public void finalizeRenderLists(Viewport viewport) { + if (this.sectionCollector != null) { + this.renderLists = this.sectionCollector.createRenderLists(viewport); + this.lastSectionCollector = this.sectionCollector; + this.sectionCollector = null; + } + } + + private boolean isOutOfGraph(SectionPos pos) { + var sectionY = pos.getY(); + // 1.21.1 has no getMinSectionY/getMaxSectionY. + // in 1.21.1 we have: getMinSection/getMaxSection, which are half-open [min, max). + return this.level.getMinSection() <= sectionY && sectionY < this.level.getMaxSection() && !this.sectionByPosition.containsKey(pos.asLong()); } private float getSearchDistance() { @@ -167,8 +235,7 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { BlockPos origin = camera.getBlockPosition(); if (spectator && this.level.getBlockState(origin) - .isSolidRender(this.level, origin)) - { + .isSolidRender(this.level, origin)) { useOcclusionCulling = false; } else { useOcclusionCulling = Minecraft.getInstance().smartCull; @@ -176,6 +243,10 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { return useOcclusionCulling; } + public void beforeSectionUpdates() { + this.renderableSectionTree.ensureCapacity(this.getRenderDistance()); + } + private void resetRenderLists() { this.renderLists = SortedRenderLists.empty(); @@ -204,12 +275,14 @@ public void onSectionAdded(int x, int y, int z) { if (section.hasOnlyAir()) { this.updateSectionInfo(renderSection, BuiltSectionInfo.EMPTY); } else { - renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD); + this.renderableSectionTree.add(renderSection); + renderSection.setPendingUpdate(ChunkUpdateTypes.INITIAL_BUILD, this.lastFrameAtTime); } this.connectNeighborNodes(renderSection); - this.needsGraphUpdate = true; + // force update to schedule build task + this.markGraphDirty(); } public void onSectionRemoved(int x, int y, int z) { @@ -220,6 +293,8 @@ public void onSectionRemoved(int x, int y, int z) { return; } + this.renderableSectionTree.remove(x, y, z); + if (section.getTranslucentData() != null) { this.sortTriggering.removeSection(section.getTranslucentData(), sectionPos); } @@ -235,14 +310,15 @@ public void onSectionRemoved(int x, int y, int z) { section.delete(); - this.needsGraphUpdate = true; + // force update to remove section from render lists + this.markGraphDirty(); } public void renderLayer(ChunkRenderMatrices matrices, TerrainRenderPass pass, double x, double y, double z) { RenderDevice device = RenderDevice.INSTANCE; CommandList commandList = device.createCommandList(); - this.chunkRenderer.render(matrices, commandList, this.renderLists, pass, new CameraTransform(x, y, z)); + this.chunkRenderer.render(matrices, commandList, this.renderLists, pass, new CameraTransform(x, y, z), this.sortBehavior != SortBehavior.OFF); commandList.flush(); } @@ -274,7 +350,7 @@ public void tickVisibleRenders() { } for (TextureAtlasSprite sprite : sprites) { - SpriteUtil.markSpriteActive(sprite); + SpriteUtil.INSTANCE.markSpriteActive(sprite); } } } @@ -301,24 +377,80 @@ public void uploadChunks() { // (sort results never change the graph) // generally there's no sort results without a camera movement, which would also trigger // a graph update, but it can sometimes happen because of async task execution - this.needsGraphUpdate = this.needsGraphUpdate || this.processChunkBuildResults(results); + this.needsGraphUpdate |= this.processChunkBuildResults(results); for (var result : results) { result.destroy(); } } + private boolean sectionVisible(RenderSection section) { + // unloaded sections are considered visible as to not be an impossible requirement for immediate presentation + return section == null || section.getLastVisibleFrame() == this.lastUpdatedFrame; + } + + private boolean isSectionImmediatePresentationCandidate(RenderSection section) { + if (this.cameraPosition == null) { + return false; + } + var distanceSquared = section.getSquaredDistance( + (float) this.cameraPosition.x(), + (float) this.cameraPosition.y(), + (float) this.cameraPosition.z() + ); + + if (distanceSquared < NEARBY_REBUILD_DISTANCE) { + return true; + } + + return distanceSquared < IMMEDIATE_PRESENT_DISTANCE && + // check that visible or adjacent to a visible section + (this.sectionVisible(section) + || this.sectionVisible(section.adjacentDown) + || this.sectionVisible(section.adjacentUp) + || this.sectionVisible(section.adjacentNorth) + || this.sectionVisible(section.adjacentSouth) + || this.sectionVisible(section.adjacentWest) + || this.sectionVisible(section.adjacentEast)); + } + private boolean processChunkBuildResults(ArrayList results) { var filtered = filterChunkBuildResults(results); + var start = System.nanoTime(); this.regions.uploadResults(RenderDevice.INSTANCE.createCommandList(), filtered); + var uploadDuration = System.nanoTime() - start; boolean touchedSectionInfo = false; + long totalUploadSize = 0; for (var result : filtered) { + var resultSize = result.getResultSize(); + var job = result.render.getRunningJob(); + TranslucentData oldData = result.render.getTranslucentData(); if (result instanceof ChunkBuildOutput chunkBuildOutput) { - this.updateSectionInfo(result.render, chunkBuildOutput.info); - touchedSectionInfo = true; + var prevFlags = result.render.getFlags(); + + touchedSectionInfo |= this.updateSectionInfo(result.render, chunkBuildOutput.info); + + // if result was blocking (or is approximately visible) and section is now newly renderable, force render it since it's probably a newly uncovered chunk. + // This also fixes flickering issues with pistons moving blocks and switching between being a mesh and a BE. + if (job != null + && (job.isBlocking() || this.isSectionImmediatePresentationCandidate(result.render))) { + // make sure only to add the section's new render flags to avoid duplicate entries in the render list + var newFlags = RenderSectionFlags.getNewRenderFlags(prevFlags, chunkBuildOutput.info.flags); + if (newFlags != 0) { + // if there is currently no section collector since there was no graph traversal, + // reuse the previous section collector and use it to generate new extended render lists + if (this.sectionCollector == null) { + this.sectionCollector = this.lastSectionCollector; + } + this.sectionCollector.visitWithFlags(result.render, newFlags); + } + } + + result.render.setLastMeshResultSize(resultSize); + this.meshTaskSizeEstimator.addData(this.meshTaskSizeEstimator.resultForSection(result.render, resultSize)); if (chunkBuildOutput.translucentData != null) { this.sortTriggering.integrateTranslucentData(oldData, chunkBuildOutput.translucentData, this.cameraPosition, this::scheduleSort); @@ -327,32 +459,47 @@ private boolean processChunkBuildResults(ArrayList results) { result.render.setTranslucentData(chunkBuildOutput.translucentData); } } else if (result instanceof ChunkSortOutput sortOutput - && sortOutput.getTopoSorter() != null + && sortOutput.getDynamicSorter() != null && result.render.getTranslucentData() instanceof DynamicTopoData data) { - this.sortTriggering.applyTriggerChanges(data, sortOutput.getTopoSorter(), result.render.getPosition(), this.cameraPosition); + this.sortTriggering.applyTriggerChanges(data, sortOutput.getDynamicSorter(), result.render.getPosition(), this.cameraPosition); } - var job = result.render.getTaskCancellationToken(); - - // clear the cancellation token (thereby marking the section as not having an - // active task) if this job is the most recent submitted job for this section + // clear the running job if this job is the most recent submitted job for this section if (job != null && result.submitTime >= result.render.getLastSubmittedFrame()) { - result.render.setTaskCancellationToken(null); + result.render.setRunningJob(null); } result.render.setLastUploadFrame(result.submitTime); + + totalUploadSize += resultSize; + } + + this.meshTaskSizeEstimator.updateModels(); + + // insert and update the upload duration estimator with the total upload size, + // since we don't know which task took how long and the time it takes to upload is not independent between tasks + // we take the average size and duration + if (!filtered.isEmpty()) { + this.jobUploadDurationEstimator.addData(new UploadDuration(uploadDuration / filtered.size(), totalUploadSize / filtered.size())); + this.jobUploadDurationEstimator.updateModels(); } return touchedSectionInfo; } - private void updateSectionInfo(RenderSection render, BuiltSectionInfo info) { - render.setInfo(info); + private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) { + if (info == null || !RenderSectionFlags.needsRender(info.flags)) { + this.renderableSectionTree.remove(render); + } else { + this.renderableSectionTree.add(render); + } + + var infoChanged = render.setInfo(info); if (info == null || ArrayUtils.isEmpty(info.globalBlockEntities)) { - this.sectionsWithGlobalEntities.remove(render); + return this.sectionsWithGlobalEntities.remove(render) || infoChanged; } else { - this.sectionsWithGlobalEntities.add(render); + return this.sectionsWithGlobalEntities.add(render) || infoChanged; } } @@ -378,12 +525,19 @@ private static List filterChunkBuildResults(ArrayList collectChunkBuildResults() { ArrayList results = new ArrayList<>(); + ChunkJobResult result; while ((result = this.buildResults.poll()) != null) { results.add(result.unwrap()); + var jobEffort = result.getJobEffort(); + if (jobEffort != null) { + this.jobDurationEstimator.addData(jobEffort); + } } + this.jobDurationEstimator.updateModels(); + return results; } @@ -393,6 +547,10 @@ public void cleanupAndFlip() { } public void updateChunks(boolean updateImmediately) { + this.thisFrameBlockingTasks = 0; + this.nextFrameBlockingTasks = 0; + this.deferredTasks = 0; + var thisFrameBlockingCollector = this.lastBlockingCollector; this.lastBlockingCollector = null; if (thisFrameBlockingCollector == null) { @@ -402,23 +560,27 @@ public void updateChunks(boolean updateImmediately) { if (updateImmediately) { // for a perfect frame where everything is finished use the last frame's blocking collector // and add all tasks to it so that they're waited on - this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector, UnlimitedResourceBudget.INSTANCE); + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { + var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); + + // an estimator is used estimate task duration and limit the execution time to the available worker capacity. + // separately, tasks are limited by their estimated upload size and duration. + var uploadBudget = new LimitedResourceBudget( + Math.max((long) (this.averageFrameDuration * FRAME_DURATION_UPLOAD_FRACTION), MIN_UPLOAD_DURATION_BUDGET), + this.regions.getStagingBuffer().getUploadSizeLimit(this.averageFrameDuration)); + var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); - var deferredCollector = new ChunkJobCollector( - this.builder.getHighEffortSchedulingBudget(), - this.builder.getLowEffortSchedulingBudget(), - this.buildResults::add); - - // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. - // otherwise submit with the collector that the next frame is blocking on. - if (SodiumClientMod.options().performance.getSortBehavior().getDeferMode() == DeferMode.ZERO_FRAMES) { - this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); - } else { - this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); - } + var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); + + this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector, uploadBudget); + + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); + this.nextFrameBlockingTasks = nextFrameBlockingCollector.getSubmittedTaskCount(); + this.deferredTasks = deferredCollector.getSubmittedTaskCount(); // wait on this frame's blocking collector which contains the important tasks from this frame // and semi-important tasks from the last frame @@ -430,79 +592,87 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( - ChunkJobCollector importantCollector, - ChunkJobCollector semiImportantCollector, - ChunkJobCollector deferredCollector) { - this.submitSectionTasks(importantCollector, ChunkUpdateType.IMPORTANT_SORT, true); - this.submitSectionTasks(semiImportantCollector, ChunkUpdateType.IMPORTANT_REBUILD, true); - - // since the sort tasks are run last, the effort category can be ignored and - // simply fills up the remaining budget. Splitting effort categories is still - // important to prevent high effort tasks from using up the entire budget if it - // happens to divide evenly. - this.submitSectionTasks(deferredCollector, ChunkUpdateType.REBUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.INITIAL_BUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.SORT, true); + ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector, UploadResourceBudget uploadBudget) { + this.submitSectionTasks(importantCollector, uploadBudget, TaskQueueType.ZERO_FRAME_DEFER); + this.submitSectionTasks(semiImportantCollector, uploadBudget, TaskQueueType.ONE_FRAME_DEFER); + this.submitSectionTasks(deferredCollector, uploadBudget, TaskQueueType.ALWAYS_DEFER); + this.submitSectionTasks(deferredCollector, uploadBudget, TaskQueueType.INITIAL_BUILD); } - private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType type, boolean ignoreEffortCategory) { - var queue = this.taskLists.get(type); + private void submitSectionTasks(ChunkJobCollector collector, UploadResourceBudget uploadBudget, TaskQueueType queueType) { + var taskList = this.taskLists.get(queueType); - while (!queue.isEmpty() && collector.hasBudgetFor(type.getTaskEffort(), ignoreEffortCategory)) { - RenderSection section = queue.remove(); + // submit tasks as long as there's tasks available, the collector has worker thread budget, and there's enough upload budget left + while (!taskList.isEmpty() && collector.hasBudgetRemaining() && (uploadBudget.isAvailable() || queueType.allowsUnlimitedUploadDuration())) { + RenderSection section = taskList.poll(); - if (section.isDisposed()) { - continue; + if (section == null) { + break; } - // stop if the section is in this list but doesn't have this update type + // don't schedule tasks for sections that don't need it anymore, + // since the pending update it cleared when a task is started, this includes + // sections for which there's a currently running task. var pendingUpdate = section.getPendingUpdate(); - if (pendingUpdate != null && pendingUpdate != type) { - continue; + if (pendingUpdate != 0) { + this.submitSectionTask(collector, section, pendingUpdate, uploadBudget, queueType == TaskQueueType.ZERO_FRAME_DEFER); } + } + } - int frame = this.lastUpdatedFrame; - ChunkBuilderTask task; - if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { - task = this.createSortTask(section, frame); + private void submitSectionTask(ChunkJobCollector collector, @NotNull RenderSection section, int type, UploadResourceBudget uploadBudget, boolean blocking) { + if (section.isDisposed()) { + return; + } - if (task == null) { - // when a sort task is null it means the render section has no dynamic data and - // doesn't need to be sorted. Nothing needs to be done. - continue; - } - } else { - task = this.createRebuildTask(section, frame); - - if (task == null) { - // if the section is empty or doesn't exist submit this null-task to set the - // built flag on the render section. - // It's important to use a NoData instead of null translucency data here in - // order for it to clear the old data from the translucency sorting system. - // This doesn't apply to sorting tasks as that would result in the section being - // marked as empty just because it was scheduled to be sorted and its dynamic - // data has since been removed. In that case simply nothing is done as the - // rebuild that must have happened in the meantime includes new non-dynamic - // index data. - var result = ChunkJobResult.successfully(new ChunkBuildOutput( - section, frame, NoData.forEmptySection(section.getPosition()), - BuiltSectionInfo.EMPTY, Collections.emptyMap())); - this.buildResults.add(result); - - section.setTaskCancellationToken(null); + ChunkBuilderTask task; + if (ChunkUpdateTypes.isInitialBuild(type) || ChunkUpdateTypes.isRebuild(type)) { + task = this.createRebuildTask(section, this.frame); + + if (task == null) { + // if the section is empty or doesn't exist submit this null-task to set the + // built flag on the render section. + // It's important to use a NoData instead of null translucency data here in + // order for it to clear the old data from the translucency sorting system. + // This doesn't apply to sorting tasks as that would result in the section being + // marked as empty just because it was scheduled to be sorted and its dynamic + // data has since been removed. In that case simply nothing is done as the + // rebuild that must have happened in the meantime includes new non-dynamic + // index data. + TranslucentData translucentData = null; + if (this.sortBehavior != SortBehavior.OFF) { + translucentData = NoData.forEmptySection(section.getPosition()); } + var result = ChunkJobResult.successfully(new ChunkBuildOutput( + section, this.frame, translucentData, + BuiltSectionInfo.EMPTY, Collections.emptyMap())); + this.buildResults.add(result); + + section.setRunningJob(null); + } + } else { // implies it's a type of sort task + task = this.createSortTask(section, this.frame); + + if (task == null) { + // when a sort task is null it means the render section has no dynamic data and + // doesn't need to be sorted. Nothing needs to be done. + section.clearPendingUpdate(); + return; } + } - if (task != null) { - var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); - collector.addSubmittedJob(job); + if (task != null) { + var job = this.builder.scheduleTask(task, ChunkUpdateTypes.isImportant(type), collector::onJobFinished, blocking); + collector.addSubmittedJob(job); - section.setTaskCancellationToken(job); - } + // consume upload budget in size and duration using estimates + uploadBudget.consume(job.getEstimatedUploadDuration(), job.getEstimatedSize()); - section.setLastSubmittedFrame(frame); - section.setPendingUpdate(null); + section.setRunningJob(job); } + + section.setLastSubmittedFrame(this.frame); + section.clearPendingUpdate(); } public @Nullable ChunkBuilderMeshingTask createRebuildTask(RenderSection render, int frame) { @@ -512,15 +682,23 @@ private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType typ return null; } - return new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); + var task = new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context, this.sortBehavior, ChunkUpdateTypes.isRebuildWithSort(render.getPendingUpdate())); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator, this.jobUploadDurationEstimator); + return task; } public ChunkBuilderSortingTask createSortTask(RenderSection render, int frame) { - return ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + var task = ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + if (task != null) { + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator, this.jobUploadDurationEstimator); + } + return task; } public void processGFNIMovement(CameraMovement movement) { - this.sortTriggering.triggerSections(this::scheduleSort, movement); + if (this.sortTriggering != null) { + this.sortTriggering.triggerSections(this::scheduleSort, movement); + } } public void markGraphDirty() { @@ -571,25 +749,43 @@ public int getVisibleChunkCount() { return sections; } + private boolean upgradePendingUpdate(RenderSection section, int updateType) { + if (updateType == 0) { + return false; + } + + var current = section.getPendingUpdate(); + var joined = ChunkUpdateTypes.join(current, updateType); + + if (joined == current) { + return false; + } + + section.setPendingUpdate(joined, this.lastFrameAtTime); + + // mark graph as dirty so that it picks up the section's pending task + this.markGraphDirty(); + + return true; + } + public void scheduleSort(long sectionPos, boolean isDirectTrigger) { RenderSection section = this.sectionByPosition.get(sectionPos); if (section != null) { - var pendingUpdate = ChunkUpdateType.SORT; - var priorityMode = SodiumClientMod.options().performance.getSortBehavior().getPriorityMode(); - if (priorityMode == PriorityMode.ALL - || priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { - pendingUpdate = ChunkUpdateType.IMPORTANT_SORT; + int pendingUpdate = ChunkUpdateTypes.SORT; + var priorityMode = this.sortBehavior.getPriorityMode(); + if (priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE) || priorityMode == PriorityMode.ALL) { + pendingUpdate = ChunkUpdateTypes.join(pendingUpdate, ChunkUpdateTypes.IMPORTANT); } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); + + if (this.upgradePendingUpdate(section, pendingUpdate)) { section.prepareTrigger(isDirectTrigger); } } } - public void scheduleRebuild(int x, int y, int z, boolean important) { + public void scheduleRebuild(int x, int y, int z, boolean playerChanged) { RenderAsserts.validateCurrentThread(); this.sectionCache.invalidate(x, y, z); @@ -597,32 +793,24 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { RenderSection section = this.sectionByPosition.get(SectionPos.asLong(x, y, z)); if (section != null && section.isBuilt()) { - ChunkUpdateType pendingUpdate; + int pendingUpdate; - if (allowImportantRebuilds() && (important || this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE))) { - pendingUpdate = ChunkUpdateType.IMPORTANT_REBUILD; + if (playerChanged && this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE)) { + pendingUpdate = ChunkUpdateTypes.join(ChunkUpdateTypes.REBUILD, ChunkUpdateTypes.IMPORTANT); } else { - pendingUpdate = ChunkUpdateType.REBUILD; + pendingUpdate = ChunkUpdateTypes.REBUILD; } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); - - this.needsGraphUpdate = true; - } + this.upgradePendingUpdate(section, pendingUpdate); } } - private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); - private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); - private boolean shouldPrioritizeTask(RenderSection section, float distance) { - return this.cameraBlockPos != null && section.getSquaredDistance(this.cameraBlockPos) < distance; - } - - private static boolean allowImportantRebuilds() { - return !SodiumClientMod.options().performance.alwaysDeferChunkUpdates; + return this.cameraPosition != null && section.getSquaredDistance( + (float) this.cameraPosition.x(), + (float) this.cameraPosition.y(), + (float) this.cameraPosition.z() + ) < distance; } private float getEffectiveRenderDistance() { @@ -676,8 +864,10 @@ public Collection getDebugStrings() { int count = 0; - long deviceUsed = 0; - long deviceAllocated = 0; + long geometryDeviceUsed = 0; + long geometryDeviceAllocated = 0; + long indexDeviceUsed = 0; + long indexDeviceAllocated = 0; for (var region : this.regions.getLoadedRegions()) { var resources = region.getResources(); @@ -686,29 +876,48 @@ public Collection getDebugStrings() { continue; } - var buffer = resources.getGeometryArena(); + var geometryArena = resources.getGeometryArena(); + geometryDeviceUsed += geometryArena.getDeviceUsedMemory(); + geometryDeviceAllocated += geometryArena.getDeviceAllocatedMemory(); - deviceUsed += buffer.getDeviceUsedMemory(); - deviceAllocated += buffer.getDeviceAllocatedMemory(); + var indexArena = resources.getIndexArena(); + indexDeviceUsed += indexArena.getDeviceUsedMemory(); + indexDeviceAllocated += indexArena.getDeviceAllocatedMemory(); count++; } - list.add(String.format("Geometry Pool: %d/%d MiB (%d buffers)", MathUtil.toMib(deviceUsed), MathUtil.toMib(deviceAllocated), count)); + list.add(String.format("Pools: Geometry %d/%d MiB, Index %d/%d MiB (%d buffers)", + MathUtil.toMib(geometryDeviceUsed), MathUtil.toMib(geometryDeviceAllocated), + MathUtil.toMib(indexDeviceUsed), MathUtil.toMib(indexDeviceAllocated), count)); list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString())); - list.add(String.format("Chunk Builder: Permits=%02d (E %03d) | Busy=%02d | Total=%02d", - this.builder.getScheduledJobCount(), this.builder.getScheduledEffort(), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) + list.add(String.format("Chunk Builder: Schd=%02d | Busy=%02d (%04d%%) | Total=%02d", + this.builder.getScheduledJobCount(), this.builder.getBusyThreadCount(), (int) (this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getTotalThreadCount()) ); - list.add(String.format("Chunk Queues: U=%02d (P0=%03d | P1=%03d | P2=%03d)", - this.buildResults.size(), - this.taskLists.get(ChunkUpdateType.IMPORTANT_REBUILD).size() + this.taskLists.get(ChunkUpdateType.IMPORTANT_SORT).size(), - this.taskLists.get(ChunkUpdateType.REBUILD).size() + this.taskLists.get(ChunkUpdateType.SORT).size(), - this.taskLists.get(ChunkUpdateType.INITIAL_BUILD).size()) + list.add(String.format("Tasks: N0=%03d | N1=%03d | Def=%03d, Recv=%03d", + this.thisFrameBlockingTasks, this.nextFrameBlockingTasks, this.deferredTasks, this.buildResults.size()) ); - this.sortTriggering.addDebugStrings(list); + if (PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment()) { + var meshTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderMeshingTask.class); + var sortTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderSortingTask.class); + var uploadDurationParameters = this.jobUploadDurationEstimator.toString(null); + list.add(String.format("Duration: Mesh %s, Sort %s, Upload %s", meshTaskParameters, sortTaskParameters, uploadDurationParameters)); + + var sizeEstimates = new ReferenceArrayList(); + for (var type : MeshResultSize.SectionCategory.values()) { + sizeEstimates.add(String.format("%s=%s", type, this.meshTaskSizeEstimator.toString(type))); + } + list.add(String.format("Size: %s", String.join(", ", sizeEstimates))); + } + + if (this.sortBehavior != SortBehavior.OFF) { + this.sortTriggering.addDebugStrings(list, this.sortBehavior); + } else { + list.add("TS OFF"); + } return list; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ShaderChunkRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ShaderChunkRenderer.java index 675e8ae632..708ca6987b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ShaderChunkRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ShaderChunkRenderer.java @@ -4,11 +4,12 @@ import net.caffeinemc.mods.sodium.client.gl.attribute.GlVertexFormat; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; +import net.caffeinemc.mods.sodium.client.gl.shader.*; import net.caffeinemc.mods.sodium.client.render.chunk.shader.*; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; -import net.caffeinemc.mods.sodium.client.gl.shader.*; import net.minecraft.resources.ResourceLocation; + import java.util.Map; public abstract class ShaderChunkRenderer implements ChunkRenderer { @@ -38,7 +39,7 @@ protected GlProgram compileProgram(ChunkShaderOptions opti } private GlProgram createShader(String path, ChunkShaderOptions options) { - ShaderConstants constants = options.constants(); + ShaderConstants constants = createShaderConstants(options); GlShader vertShader = ShaderLoader.loadShader(ShaderType.VERTEX, ResourceLocation.fromNamespaceAndPath("sodium", path + ".vsh"), constants); @@ -62,6 +63,20 @@ private GlProgram createShader(String path, ChunkShaderOpt } } + private static ShaderConstants createShaderConstants(ChunkShaderOptions options) { + ShaderConstants.Builder builder = ShaderConstants.builder(); + builder.addAll(options.fog().getDefines()); + + if (options.pass().supportsFragmentDiscard()) { + builder.add("USE_FRAGMENT_DISCARD"); + } + + builder.add("USE_VERTEX_COMPRESSION"); // TODO: allow compact vertex format to be disabled + builder.add("MAX_TEXTURE_LOD_BIAS", String.valueOf(RenderDevice.INSTANCE.getMaxTextureLodBias())); + + return builder.build(); + } + protected void begin(TerrainRenderPass pass) { pass.startDrawing(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/SharedQuadIndexBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/SharedQuadIndexBuffer.java index e21dc96476..8985b13244 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/SharedQuadIndexBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/SharedQuadIndexBuffer.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.tessellation.GlIndexType; import net.caffeinemc.mods.sodium.client.gl.util.EnumBitField; +import net.caffeinemc.mods.sodium.client.util.NativeBuffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -55,6 +56,14 @@ private void grow(CommandList commandList, int primitiveCount) { this.maxPrimitives = primitiveCount; } + public static NativeBuffer createIndexBuffer(IndexType indexType, int primitiveCount) { + var bufferSize = primitiveCount * indexType.getBytesPerElement() * ELEMENTS_PER_PRIMITIVE; + var buffer = new NativeBuffer(bufferSize); + + indexType.createIndexBuffer(buffer.getDirectBuffer(), primitiveCount); + + return buffer; + } public GlBuffer getBufferObject() { return this.buffer; @@ -64,14 +73,6 @@ public void delete(CommandList commandList) { commandList.deleteBuffer(this.buffer); } - public GlIndexType getIndexFormat() { - return this.indexType.getFormat(); - } - - public IndexType getIndexType() { - return this.indexType; - } - public enum IndexType { SHORT(GlIndexType.UNSIGNED_SHORT, 64 * 1024) { @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/TaskQueueType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/TaskQueueType.java new file mode 100644 index 0000000000..648bf05b5d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/TaskQueueType.java @@ -0,0 +1,16 @@ +package net.caffeinemc.mods.sodium.client.render.chunk; + +public enum TaskQueueType { + ZERO_FRAME_DEFER, + ONE_FRAME_DEFER, + ALWAYS_DEFER, + INITIAL_BUILD; + + public boolean allowsUnlimitedUploadDuration() { + return this == ZERO_FRAME_DEFER; + } + + public int queueSizeLimit() { + return this == INITIAL_BUILD ? 128 : Integer.MAX_VALUE; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java index 102f93727a..89fcc25d5f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java @@ -1,10 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; public abstract class BuilderTaskOutput { public final RenderSection render; public final int submitTime; + private long resultSize = MeshResultSize.NO_DATA; public BuilderTaskOutput(RenderSection render, int buildTime) { this.render = render; @@ -13,4 +15,13 @@ public BuilderTaskOutput(RenderSection render, int buildTime) { public void destroy() { } + + protected abstract long calculateResultSize(); + + public long getResultSize() { + if (this.resultSize == MeshResultSize.NO_DATA) { + this.resultSize = this.calculateResultSize(); + } + return this.resultSize; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java index eae5389deb..672b47784c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java @@ -9,20 +9,20 @@ import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.UpdatedQuadsList; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - /** * 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 { + private static final int UNASSIGNED_SEGMENT_INDEX = ModelQuadFacing.UNASSIGNED.ordinal() << 1; + private final Reference2ReferenceOpenHashMap builders = new Reference2ReferenceOpenHashMap<>(); private final ChunkVertexType vertexType; @@ -55,52 +55,113 @@ public ChunkModelBuilder get(TerrainRenderPass pass) { return this.builders.get(pass); } + public static int[] makeVertexSegments() { + return new int[ModelQuadFacing.COUNT << 1]; + } + /** * Creates immutable baked chunk meshes from all non-empty scratch buffers. This is used after all blocks * have been rendered to pass the finished meshes over to the graphics card. This function can be called multiple * times to return multiple copies. */ - public BuiltSectionMeshParts createMesh(TerrainRenderPass pass, boolean forceUnassigned) { + public BuiltSectionMeshParts createMesh(TerrainRenderPass pass, int visibleSlices, boolean forceUnassigned, boolean sliceReordering) { var builder = this.builders.get(pass); + int[] vertexSegments = makeVertexSegments(); + int vertexTotal = 0; - List vertexBuffers = new ArrayList<>(); - int[] vertexCounts = new int[ModelQuadFacing.COUNT]; + // get the total vertex count to initialize the buffer + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + vertexTotal += builder.getVertexBuffer(facing).count(); + } - int vertexSum = 0; + if (vertexTotal == 0) { + return null; + } - for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { - var ordinal = facing.ordinal(); - var buffer = builder.getVertexBuffer(facing); + var mergedBuffer = new NativeBuffer(vertexTotal * this.vertexType.getVertexFormat().getStride()); + var mergedBufferBuilder = mergedBuffer.getDirectBuffer(); - if (buffer.isEmpty()) { - continue; + if (sliceReordering) { + // sliceReordering implies !forceUnassigned + + // write all currently visible slices first, and then the rest. + // start with unassigned as it will never become invisible + var unassignedBuffer = builder.getVertexBuffer(ModelQuadFacing.UNASSIGNED); + int vertexSegmentCount = 0; + vertexSegments[vertexSegmentCount++] = unassignedBuffer.count(); + vertexSegments[vertexSegmentCount++] = ModelQuadFacing.UNASSIGNED.ordinal(); + if (!unassignedBuffer.isEmpty()) { + mergedBufferBuilder.put(unassignedBuffer.slice()); } - vertexBuffers.add(buffer.slice()); - var bufferCount = buffer.count(); - if (!forceUnassigned) { - vertexCounts[ordinal] = bufferCount; + // write all visible and then invisible slices + for (var step = 0; step < 2; step++) { + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + var facingIndex = facing.ordinal(); + if (facing == ModelQuadFacing.UNASSIGNED || ((visibleSlices >> facingIndex) & 1) == step) { + continue; + } + + var buffer = builder.getVertexBuffer(facing); + + // generate empty ranges to prevent SectionRenderData storage from making up indexes for null ranges + vertexSegments[vertexSegmentCount++] = buffer.count(); + vertexSegments[vertexSegmentCount++] = facingIndex; + + if (!buffer.isEmpty()) { + mergedBufferBuilder.put(buffer.slice()); + } + } } + } else { + // forceUnassigned implies !sliceReordering - vertexSum += bufferCount; - } + if (forceUnassigned) { + vertexSegments[UNASSIGNED_SEGMENT_INDEX] = vertexTotal; + vertexSegments[UNASSIGNED_SEGMENT_INDEX + 1] = ModelQuadFacing.UNASSIGNED.ordinal(); + } - if (vertexSum == 0) { - return null; + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + var buffer = builder.getVertexBuffer(facing); + if (!buffer.isEmpty()) { + if (!forceUnassigned) { + var facingIndex = facing.ordinal(); + var segmentIndex = facingIndex << 1; + vertexSegments[segmentIndex] = buffer.count(); + vertexSegments[segmentIndex + 1] = facingIndex; + } + mergedBufferBuilder.put(buffer.slice()); + } + } } - if (forceUnassigned) { - vertexCounts[ModelQuadFacing.UNASSIGNED.ordinal()] = vertexSum; - } + return new BuiltSectionMeshParts(mergedBuffer, vertexSegments); + } + + public BuiltSectionMeshParts createModifiedTranslucentMesh(UpdatedQuadsList updatedQuads) { + // mesh modification assumes non-empty mesh with predetermined size - var mergedBuffer = new NativeBuffer(vertexSum * this.vertexType.getVertexFormat().getStride()); + var builder = this.builders.get(DefaultTerrainRenderPasses.TRANSLUCENT); + + var stride = this.vertexType.getVertexFormat().getStride(); + var vertexTotal = TranslucentData.quadCountToVertexCount(updatedQuads.getMeshQuadCount()); + var mergedBuffer = new NativeBuffer(vertexTotal * stride); var mergedBufferBuilder = mergedBuffer.getDirectBuffer(); - for (var buffer : vertexBuffers) { - mergedBufferBuilder.put(buffer); + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + var buffer = builder.getVertexBuffer(facing); + if (!buffer.isEmpty()) { + mergedBufferBuilder.put(buffer.slice()); + } } - return new BuiltSectionMeshParts(mergedBuffer, vertexCounts); + updatedQuads.applyBufferUpdates(builder.getVertexBuffer(ModelQuadFacing.UNASSIGNED), mergedBufferBuilder); + + int[] vertexSegments = makeVertexSegments(); + vertexSegments[UNASSIGNED_SEGMENT_INDEX] = vertexTotal; + vertexSegments[UNASSIGNED_SEGMENT_INDEX + 1] = ModelQuadFacing.UNASSIGNED.ordinal(); + + return new BuiltSectionMeshParts(mergedBuffer, vertexSegments); } public void destroy() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java index 88102d1d16..20f075681d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java @@ -40,4 +40,17 @@ public void destroy() { data.getVertexData().free(); } } + + private long getMeshSize() { + long size = 0; + for (var data : this.meshes.values()) { + size += data.getVertexData().getLength(); + } + return size; + } + + @Override + public long calculateResultSize() { + return super.calculateResultSize() + this.getMeshSize(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java index 52236a161e..8c9607f0d4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java @@ -2,14 +2,11 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.SortData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.Sorter; -import net.caffeinemc.mods.sodium.client.util.NativeBuffer; -public class ChunkSortOutput extends BuilderTaskOutput implements SortData { - private NativeBuffer indexBuffer; +public class ChunkSortOutput extends BuilderTaskOutput { + private Sorter sorter; private boolean reuseUploadedIndexData; - private DynamicTopoData.DynamicTopoSorter topoSorter; public ChunkSortOutput(RenderSection render, int buildTime) { super(render, buildTime); @@ -17,41 +14,46 @@ public ChunkSortOutput(RenderSection render, int buildTime) { public ChunkSortOutput(RenderSection render, int buildTime, Sorter data) { this(render, buildTime); - this.copyResultFrom(data); + this.setSorter(data); } - public void copyResultFrom(Sorter sorter) { - this.indexBuffer = sorter.getIndexBuffer(); + public void setSorter(Sorter sorter) { + this.sorter = sorter; this.reuseUploadedIndexData = false; - if (sorter instanceof DynamicTopoData.DynamicTopoSorter topoSorterInstance) { - this.topoSorter = topoSorterInstance; - } } - public void markAsReusingUploadedData() { - this.reuseUploadedIndexData = true; + public Sorter getSorter() { + return this.sorter; } - @Override - public NativeBuffer getIndexBuffer() { - return this.indexBuffer; + public void markAsReusingUploadedData() { + this.reuseUploadedIndexData = true; } - @Override public boolean isReusingUploadedIndexData() { return this.reuseUploadedIndexData; } - public DynamicTopoData.DynamicTopoSorter getTopoSorter() { - return this.topoSorter; + public DynamicTopoData.DynamicTopoSorter getDynamicSorter() { + return this.sorter instanceof DynamicTopoData.DynamicTopoSorter dynamicSorter ? dynamicSorter : null; } - @Override public void destroy() { super.destroy(); + if (this.sorter != null) { + this.sorter.destroy(); + } + } - if (this.indexBuffer != null) { - this.indexBuffer.free(); + @Override + protected long calculateResultSize() { + if (this.sorter == null) { + return 0; + } + var indexBuffer = this.sorter.getIndexBuffer(); + if (indexBuffer == null) { + return 0; } + return indexBuffer.getLength(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuilder.java index 36e5aad24d..cee91b9a6e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/BakedChunkModelBuilder.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.jetbrains.annotations.NotNull; public class BakedChunkModelBuilder implements ChunkModelBuilder { private final ChunkMeshBufferBuilder[] vertexBuffers; @@ -24,14 +25,14 @@ public ChunkMeshBufferBuilder getVertexBuffer(ModelQuadFacing facing) { } @Override - public void addSprite(TextureAtlasSprite sprite) { + public void addSprite(@NotNull TextureAtlasSprite sprite) { this.renderData.addSprite(sprite); } @Override public VertexConsumer asFallbackVertexConsumer(Material material, TranslucentGeometryCollector collector) { - fallbackVertexConsumer.setData(material, collector); - return fallbackVertexConsumer; + this.fallbackVertexConsumer.setData(material, collector); + return this.fallbackVertexConsumer; } public void destroy() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuilder.java index 637d1ce082..3b2f4e44b8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkModelBuilder.java @@ -6,11 +6,12 @@ import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.jetbrains.annotations.NotNull; public interface ChunkModelBuilder { ChunkMeshBufferBuilder getVertexBuffer(ModelQuadFacing facing); - void addSprite(TextureAtlasSprite sprite); + void addSprite(@NotNull TextureAtlasSprite sprite); /** * This method should not be used unless absolutely necessary! It exists only for compatibility purposes. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkVertexConsumer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkVertexConsumer.java index 48ec0c9a3e..5cfccb288c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkVertexConsumer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/buffers/ChunkVertexConsumer.java @@ -45,7 +45,7 @@ public void setData(Material material, TranslucentGeometryCollector collector) { vertex.z = z; vertex.ao = 1.0f; this.writtenAttributes |= ATTRIBUTE_POSITION_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } // Writing color ignores alpha since alpha is used as a color multiplier by Sodium. @@ -54,7 +54,7 @@ public void setData(Material material, TranslucentGeometryCollector collector) { ChunkVertexEncoder.Vertex vertex = this.vertices[this.vertexIndex]; vertex.color = ColorABGR.pack(red, green, blue, alpha); this.writtenAttributes |= ATTRIBUTE_COLOR_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override @@ -62,7 +62,7 @@ public void setData(Material material, TranslucentGeometryCollector collector) { ChunkVertexEncoder.Vertex vertex = this.vertices[this.vertexIndex]; vertex.color = ColorABGR.pack(red, green, blue, alpha); this.writtenAttributes |= ATTRIBUTE_COLOR_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override @@ -70,7 +70,7 @@ public void setData(Material material, TranslucentGeometryCollector collector) { ChunkVertexEncoder.Vertex vertex = this.vertices[this.vertexIndex]; vertex.color = ColorARGB.toABGR(argb); this.writtenAttributes |= ATTRIBUTE_COLOR_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override @@ -79,18 +79,18 @@ public void setData(Material material, TranslucentGeometryCollector collector) { vertex.u = u; vertex.v = v; this.writtenAttributes |= ATTRIBUTE_TEXTURE_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } // Overlay is ignored for chunk geometry. @Override public @NotNull VertexConsumer setUv1(int u, int v) { - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override public @NotNull VertexConsumer setOverlay(int uv) { - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override @@ -98,7 +98,7 @@ public void setData(Material material, TranslucentGeometryCollector collector) { ChunkVertexEncoder.Vertex vertex = this.vertices[this.vertexIndex]; vertex.light = ((v & 0xFFFF) << 16) | (u & 0xFFFF); this.writtenAttributes |= ATTRIBUTE_LIGHT_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override @@ -106,13 +106,13 @@ public void setData(Material material, TranslucentGeometryCollector collector) { ChunkVertexEncoder.Vertex vertex = this.vertices[this.vertexIndex]; vertex.light = uv; this.writtenAttributes |= ATTRIBUTE_LIGHT_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } @Override public @NotNull VertexConsumer setNormal(float x, float y, float z) { this.writtenAttributes |= ATTRIBUTE_NORMAL_BIT; - return potentiallyEndVertex(); + return this.potentiallyEndVertex(); } public VertexConsumer potentiallyEndVertex() { @@ -124,12 +124,14 @@ public VertexConsumer potentiallyEndVertex() { this.writtenAttributes = 0; if (this.vertexIndex == 4) { - int normal = calculateNormal(); + int normal = this.calculateNormal(); ModelQuadFacing cullFace = ModelQuadFacing.fromPackedNormal(normal); - if (this.material.isTranslucent() && this.collector != null) { - this.collector.appendQuad(normal, this.vertices, cullFace); + // let the collector intercept the quad but discard it if it's deemed invalid (i.e. not visible) + if (this.material.isTranslucent() && this.collector != null && + this.collector.appendQuad(this.vertices, cullFace, normal)) { + return this; } this.modelBuilder.getVertexBuffer(cullFace).push(this.vertices, this.material); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Abstract2DLinearEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Abstract2DLinearEstimator.java new file mode 100644 index 0000000000..2baf9c829b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Abstract2DLinearEstimator.java @@ -0,0 +1,61 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.Locale; + +public abstract class Abstract2DLinearEstimator< + C, + TBatch extends Estimator.DataBatch>, + TModel extends Abstract2DLinearEstimator.LinearFunction + > extends Estimator< + C, + Abstract2DLinearEstimator.DataPair, + TBatch, + Long, + Long, + TModel> { + protected final long initialOutput; + + public Abstract2DLinearEstimator(long initialOutput) { + this.initialOutput = initialOutput; + } + + public interface DataPair extends DataPoint { + long x(); + + long y(); + } + + protected abstract static class LinearRegressionBatch extends ObjectArrayList> implements DataBatch> { + @Override + public void addDataPoint(DataPair input) { + this.add(input); + } + } + + protected abstract static class LinearFunction>> implements Model { + protected final long initialOutput; + protected double yIntercept; + protected double slope; + protected int gatheredSamples = 0; + + public LinearFunction(long initialOutput) { + this.initialOutput = initialOutput; + } + + @Override + public Long predict(Long input) { + if (this.gatheredSamples == 0) { + return this.initialOutput; + } + + return (long) (this.yIntercept + this.slope * input); + } + + @Override + public String toString() { + return String.format(Locale.US, "s=%.2f,y=%.0f", this.slope, this.yIntercept); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java new file mode 100644 index 0000000000..f9f10c5c09 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java @@ -0,0 +1,87 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.util.MathUtil; + +import java.util.Locale; + +public abstract class Average1DEstimator extends Estimator, Average1DEstimator.ValueBatch, Void, Long, Average1DEstimator.Average> { + private final double newDataRatio; + private final long initialEstimate; + + public Average1DEstimator(double newDataRatio, long initialEstimate) { + this.newDataRatio = newDataRatio; + this.initialEstimate = initialEstimate; + } + + public interface Value extends DataPoint { + long value(); + } + + protected static class ValueBatch implements Estimator.DataBatch> { + private long valueSum; + private long count; + + @Override + public void addDataPoint(Value input) { + this.valueSum += input.value(); + this.count++; + } + + @Override + public void reset() { + this.valueSum = 0; + this.count = 0; + } + + public double getAverage() { + return ((double) this.valueSum) / this.count; + } + } + + @Override + protected ValueBatch createNewDataBatch() { + return new ValueBatch<>(); + } + + protected static class Average implements Estimator.Model> { + private final double newDataRatio; + private boolean hasRealData = false; + private double average; + + public Average(double newDataRatio, double initialValue) { + this.average = initialValue; + this.newDataRatio = newDataRatio; + } + + @Override + public void update(ValueBatch batch) { + if (batch.count > 0) { + if (this.hasRealData) { + this.average = MathUtil.exponentialMovingAverage(this.average, batch.getAverage(), this.newDataRatio); + } else { + this.average = batch.getAverage(); + this.hasRealData = true; + } + } + } + + @Override + public Long predict(Void input) { + return (long) this.average; + } + + @Override + public String toString() { + return String.format(Locale.US, "%.0f", this.average); + } + } + + @Override + protected Average createNewModel() { + return new Average<>(this.newDataRatio, this.initialEstimate); + } + + public Long predict(C category) { + return super.predict(category, null); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java new file mode 100644 index 0000000000..2f87b0e5f7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java @@ -0,0 +1,84 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import java.util.Map; + +/** + * This generic model learning class that can be used to estimate values based on a set of data points. It performs batch-wise model updates. The actual data aggregation and model updates are delegated to the implementing classes. The estimator stores multiple models in a map, one for each category. + * + * @param The type of the category key + * @param A data point contains a category and one piece of data + * @param A data batch contains multiple data points + * @param The input to the model + * @param The output of the model + * @param The model that is used to predict values + */ +public abstract class Estimator< + TCategory, + TPoint extends Estimator.DataPoint, + TBatch extends Estimator.DataBatch, + TInput, + TOutput, + TModel extends Estimator.Model> { + protected final Map models = this.createMap(); + protected final Map batches = this.createMap(); + + protected interface DataBatch { + void addDataPoint(TBatchPoint input); + + void reset(); + } + + protected interface DataPoint { + TPointCategory category(); + } + + protected interface Model { + void update(TModelBatch batch); + + TModelOutput predict(TModelInput input); + } + + protected abstract TBatch createNewDataBatch(); + + protected abstract TModel createNewModel(); + + protected abstract Map createMap(); + + public void addData(TPoint data) { + var category = data.category(); + var batch = this.batches.get(category); + if (batch == null) { + batch = this.createNewDataBatch(); + this.batches.put(category, batch); + } + batch.addDataPoint(data); + } + + private TModel ensureModel(TCategory category) { + var model = this.models.get(category); + if (model == null) { + model = this.createNewModel(); + this.models.put(category, model); + } + return model; + } + + public void updateModels() { + this.batches.forEach((category, aggregator) -> { + this.ensureModel(category).update(aggregator); + aggregator.reset(); + }); + } + + public TOutput predict(TCategory category, TInput input) { + return this.ensureModel(category).predict(input); + } + + public String toString(TCategory category) { + var model = this.models.get(category); + if (model == null) { + return "-"; + } + return model.toString(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/ExpDecayLinear2DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/ExpDecayLinear2DEstimator.java new file mode 100644 index 0000000000..4579bfcb74 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/ExpDecayLinear2DEstimator.java @@ -0,0 +1,128 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public abstract class ExpDecayLinear2DEstimator extends Abstract2DLinearEstimator< + C, + ExpDecayLinear2DEstimator.ClearingLinearRegressionBatch, + ExpDecayLinear2DEstimator.ExpDecayLinearFunction> { + private final double newDataRatio; + private final int initialSampleTarget; + private final int minBatchSize; + + public ExpDecayLinear2DEstimator(double newDataRatio, int initialSampleTarget, int minBatchSize, long initialOutput) { + super(initialOutput); + this.newDataRatio = newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.minBatchSize = minBatchSize; + } + + protected static class ClearingLinearRegressionBatch extends Abstract2DLinearEstimator.LinearRegressionBatch { + boolean deferredClear = false; + + @Override + public void reset() { + if (!this.deferredClear) { + this.clear(); + } + this.deferredClear = false; + } + + private boolean checkUpdateDefer(int minBatchSize) { + if (this.size() < minBatchSize) { + this.deferredClear = true; + return true; + } + return false; + } + } + + @Override + protected ClearingLinearRegressionBatch createNewDataBatch() { + return new ClearingLinearRegressionBatch<>(); + } + + protected static class ExpDecayLinearFunction extends Abstract2DLinearEstimator.LinearFunction< + C, + ExpDecayLinear2DEstimator.ClearingLinearRegressionBatch> { + // the maximum fraction of the total weight that new data can have + private final double newDataRatioInv; + // how many samples we want to have at least before we start diminishing the new data's weight + private final int initialSampleTarget; + private final int minBatchSize; + + private double xMeanOld = 0; + private double yMeanOld = 0; + private double covarianceOld = 0; + private double varianceOld = 0; + + public ExpDecayLinearFunction(double newDataRatio, int initialSampleTarget, int minBatchSize, long initialOutput) { + super(initialOutput); + this.newDataRatioInv = 1.0 / newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.minBatchSize = minBatchSize; + } + + @Override + public void update(ClearingLinearRegressionBatch batch) { + if (batch.isEmpty() || batch.checkUpdateDefer(this.minBatchSize)) { + return; + } + + // condition the weight to gather at least the initial sample target, and then weight the new data with a ratio + var newDataSize = batch.size(); + var totalSamples = this.gatheredSamples + newDataSize; + double oldDataWeight; + double totalWeight; + if (totalSamples <= this.initialSampleTarget) { + totalWeight = totalSamples; + oldDataWeight = this.gatheredSamples; + this.gatheredSamples = totalSamples; + } else { + oldDataWeight = newDataSize * this.newDataRatioInv - newDataSize; + totalWeight = oldDataWeight + newDataSize; + } + + double totalWeightInv = 1.0 / totalWeight; + + // calculate the weighted mean along both axes + long xSum = 0; + long ySum = 0; + for (var data : batch) { + xSum += data.x(); + ySum += data.y(); + } + double xMean = (this.xMeanOld * oldDataWeight + xSum) * totalWeightInv; + double yMean = (this.yMeanOld * oldDataWeight + ySum) * totalWeightInv; + + // the covariance and variance are calculated from the differences to the mean + double covarianceSum = 0.0; + double varianceSum = 0.0; + for (var data : batch) { + double xDelta = data.x() - xMean; + double yDelta = data.y() - yMean; + covarianceSum += xDelta * yDelta; + varianceSum += xDelta * xDelta; + } + + if (Math.abs(varianceSum) <= Double.MIN_NORMAL) { + return; + } + + covarianceSum += this.covarianceOld * oldDataWeight; + varianceSum += this.varianceOld * oldDataWeight; + + // negative slopes are clamped to produce a flat line if necessary + this.slope = Math.max(0, covarianceSum / varianceSum); + this.yIntercept = yMean - this.slope * xMean; + + this.xMeanOld = xMean; + this.yMeanOld = yMean; + this.covarianceOld = covarianceSum * totalWeightInv; + this.varianceOld = varianceSum * totalWeightInv; + } + } + + @Override + protected ExpDecayLinearFunction createNewModel() { + return new ExpDecayLinearFunction<>(this.newDataRatio, this.initialSampleTarget, this.minBatchSize, this.initialOutput); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java new file mode 100644 index 0000000000..715cb13ac7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceArrayMap; + +import java.util.Map; + +public class JobDurationEstimator extends ExpDecayLinear2DEstimator> { + public static final int INITIAL_SAMPLE_TARGET = 100; + public static final double NEW_DATA_RATIO = 0.05; + private static final int MIN_BATCH_SIZE = 40; + private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; // 5ms + + public JobDurationEstimator() { + super(NEW_DATA_RATIO, INITIAL_SAMPLE_TARGET, MIN_BATCH_SIZE, INITIAL_JOB_DURATION_ESTIMATE); + } + + public long estimateJobDuration(Class jobType, long effort) { + return this.predict(jobType, effort); + } + + @Override + protected Map, T> createMap() { + return new Reference2ReferenceArrayMap<>(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java new file mode 100644 index 0000000000..91da3a8ed1 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public record JobEffort(Class category, long duration, long effort) implements ExpDecayLinear2DEstimator.DataPair> { + public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { + return new JobEffort(effortType,System.nanoTime() - start, effort); + } + + @Override + public long x() { + return this.effort; + } + + @Override + public long y() { + return this.duration; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/LimitedResourceBudget.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/LimitedResourceBudget.java new file mode 100644 index 0000000000..f00b6cbfea --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/LimitedResourceBudget.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public class LimitedResourceBudget implements UploadResourceBudget { + private long duration; + private long size; + + public LimitedResourceBudget(long duration, long size) { + this.duration = duration; + this.size = size; + } + + @Override + public boolean isAvailable() { + return this.duration > 0 && this.size > 0; + } + + @Override + public void consume(long duration, long size) { + this.duration -= duration; + this.size -= size; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java new file mode 100644 index 0000000000..833a2a338b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java @@ -0,0 +1,47 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public record MeshResultSize(SectionCategory category, long resultSize) implements Average1DEstimator.Value { + public static long NO_DATA = -1; + + public enum SectionCategory { + LOW, + UNDERGROUND, + WATER_LEVEL, + SURFACE, + HIGH; + + public static SectionCategory forSection(RenderSection section, int seaLevelChunk) { + var sectionY = section.getChunkY(); + + // Roughly classify type of chunk based on Y level relative to sea level: + // Water level chunks are likely to have different meshes from those below and those above. + // Very low chunks are again different because they aren't going to include the underwater terrain (just caves). + // Very high chunks are (at least locally) different because they are likely to be mostly air with some terrain poking through, or just buildings/jungle tree tops. + if (sectionY == seaLevelChunk) { + return WATER_LEVEL; + } + if (sectionY < seaLevelChunk - 4) { + return LOW; + } + if (sectionY < seaLevelChunk) { + return UNDERGROUND; + } + if (sectionY < seaLevelChunk + 3) { + return SURFACE; + } + return HIGH; + } + } + + @Override + public SectionCategory category() { + return this.category; + } + + @Override + public long value() { + return this.resultSize; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java new file mode 100644 index 0000000000..16e803e3a3 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java @@ -0,0 +1,36 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.minecraft.client.multiplayer.ClientLevel; + +import java.util.EnumMap; +import java.util.Map; + +public class MeshTaskSizeEstimator extends Average1DEstimator { + public static final float NEW_DATA_RATIO = 0.02f; + + private final int seaLevelChunk; + + public MeshTaskSizeEstimator(ClientLevel level) { + super(NEW_DATA_RATIO, RenderRegion.SECTION_BUFFER_ESTIMATE); + this.seaLevelChunk = level.getSeaLevel() >> 4; + } + + public long estimateSize(RenderSection section) { + var lastResultSize = section.getLastMeshResultSize(); + if (lastResultSize != MeshResultSize.NO_DATA) { + return lastResultSize; + } + return this.predict(MeshResultSize.SectionCategory.forSection(section, this.seaLevelChunk)); + } + + public MeshResultSize resultForSection(RenderSection section, long resultSize) { + return new MeshResultSize(MeshResultSize.SectionCategory.forSection(section, this.seaLevelChunk), resultSize); + } + + @Override + protected Map createMap() { + return new EnumMap<>(MeshResultSize.SectionCategory.class); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UnlimitedResourceBudget.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UnlimitedResourceBudget.java new file mode 100644 index 0000000000..988916b435 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UnlimitedResourceBudget.java @@ -0,0 +1,15 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public class UnlimitedResourceBudget implements UploadResourceBudget { + public static final UnlimitedResourceBudget INSTANCE = new UnlimitedResourceBudget(); + + @Override + public boolean isAvailable() { + return true; // always available + } + + @Override + public void consume(long duration, long size) { + // no-op, unlimited budget means no consumption + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDuration.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDuration.java new file mode 100644 index 0000000000..36ee55ed91 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDuration.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public record UploadDuration(long uploadDuration, long size) implements ExpDecayLinear2DEstimator.DataPair { + @Override + public long x() { + return this.size; + } + + @Override + public long y() { + return this.uploadDuration; + } + + @Override + public Void category() { + return null; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDurationEstimator.java new file mode 100644 index 0000000000..352f67242d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadDurationEstimator.java @@ -0,0 +1,112 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class UploadDurationEstimator extends ExpDecayLinear2DEstimator { + public static final double NEW_DATA_RATIO = 0.05; + public static final int INITIAL_SAMPLE_TARGET = 100; + public static final int MIN_BATCH_SIZE = 100; + private static final long INITIAL_UPLOAD_TIME_ESTIMATE = 100_000L; // 100µs + + public UploadDurationEstimator() { + super(NEW_DATA_RATIO, INITIAL_SAMPLE_TARGET, MIN_BATCH_SIZE, INITIAL_UPLOAD_TIME_ESTIMATE); + } + + public long estimateUploadDuration(long size) { + return this.predict(null, size); + } + + // special map that can contain one key: null and a generic value type + private static class VoidKeyMap implements Map { + private T value; + + @Override + public int size() { + return this.value == null ? 0 : 1; + } + + @Override + public boolean isEmpty() { + return this.value == null; + } + + @Override + public boolean containsKey(Object o) { + return o == null; + } + + @Override + public boolean containsValue(Object o) { + return this.value != null && this.value.equals(o); + } + + @Override + public T get(Object o) { + if (o == null) { + return this.value; + } + return null; + } + + @Override + public @Nullable T put(Void unused, T t) { + T oldValue = this.value; + this.value = t; + return oldValue; + } + + @Override + public T remove(Object o) { + if (o == null) { + T oldValue = this.value; + this.value = null; + return oldValue; + } + return null; + } + + @Override + public void putAll(@NotNull Map map) { + if (map.containsKey(null)) { + this.value = map.get(null); + } + } + + @Override + public void clear() { + this.value = null; + } + + @Override + public @NotNull Set keySet() { + if (this.value != null) { + return Collections.singleton(null); + } + return Set.of(); + } + + @Override + public @NotNull Collection values() { + if (this.value != null) { + return Collections.singleton(this.value); + } + return Collections.emptyList(); + } + + @Override + public @NotNull Set> entrySet() { + if (this.value != null) { + return Collections.singleton(new AbstractMap.SimpleEntry<>(null, this.value)); + } + return Collections.emptySet(); + } + } + + @Override + protected Map createMap() { + return new VoidKeyMap<>(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadResourceBudget.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadResourceBudget.java new file mode 100644 index 0000000000..894f82192d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/UploadResourceBudget.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public interface UploadResourceBudget { + boolean isAvailable(); + + void consume(long duration, long size); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index 792903a5b5..76edae02c0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -17,30 +17,11 @@ import java.util.function.Consumer; public class ChunkBuilder { - /** - * The low and high efforts given to the sorting and meshing tasks, - * respectively. This split into two separate effort categories means more - * sorting tasks, which are faster, can be scheduled compared to mesh tasks. - * These values need to capture that there's a limit to how much data can be - * uploaded per frame. Since sort tasks generate index data, which is smaller - * per quad and (on average) per section, more of their results can be uploaded - * in one frame. This number should essentially be a conservative estimate of - * min((mesh task upload size) / (sort task upload size), (mesh task time) / - * (sort task time)). - */ - public static final int HIGH_EFFORT = 10; - public static final int LOW_EFFORT = 1; - public static final int EFFORT_PER_THREAD_PER_FRAME = HIGH_EFFORT + LOW_EFFORT; - private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_PER_THREAD_PER_FRAME; - static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); private final ChunkJobQueue queue = new ChunkJobQueue(); - private final List threads = new ArrayList<>(); - private final AtomicInteger busyThreadCount = new AtomicInteger(); - private final ChunkBuildContext localContext; public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { @@ -66,16 +47,8 @@ public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { * Returns the remaining effort for 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. */ - private int getTotalRemainingBudget() { - return Math.max(0, this.threads.size() * EFFORT_PER_THREAD_PER_FRAME - this.queue.getEffortSum()); - } - - public int getHighEffortSchedulingBudget() { - return Math.max(HIGH_EFFORT, (int) (this.getTotalRemainingBudget() * HIGH_EFFORT_BUDGET_FACTOR)); - } - - public int getLowEffortSchedulingBudget() { - return Math.max(LOW_EFFORT, this.getTotalRemainingBudget() - this.getHighEffortSchedulingBudget()); + public long getTotalRemainingDuration(long durationPerThread) { + return Math.max(0, this.threads.size() * durationPerThread - this.queue.getJobDurationSum()); } /** @@ -114,8 +87,7 @@ private void shutdownThreads() { this.threads.clear(); } - public , OUTPUT extends BuilderTaskOutput> ChunkJobTyped scheduleTask(TASK task, boolean important, - Consumer> consumer) + public , OUTPUT extends BuilderTaskOutput> ChunkJobTyped scheduleTask(TASK task, boolean important, Consumer> consumer, boolean blocking) { Validate.notNull(task, "Task must be non-null"); @@ -123,7 +95,7 @@ public , OUTPUT extends BuilderTaskOutput> throw new IllegalStateException("Executor is stopped"); } - var job = new ChunkJobTyped<>(task, consumer); + var job = new ChunkJobTyped<>(task, consumer, blocking); this.queue.add(job, important); @@ -169,8 +141,8 @@ public int getScheduledJobCount() { return this.queue.size(); } - public int getScheduledEffort() { - return this.queue.getEffortSum(); + public float getBusyFraction(long frameDuration) { + return (float) this.queue.getJobDurationSum() / (frameDuration * this.threads.size()); } public int getBusyThreadCount() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java index a0bcb33505..e010e29170 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java @@ -7,6 +7,12 @@ public interface ChunkJob extends CancellationToken { void execute(ChunkBuildContext context); boolean isStarted(); + + boolean isBlocking(); - int getEffort(); + long getEstimatedSize(); + + long getEstimatedDuration(); + + long getEstimatedUploadDuration(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index 69701b0a26..5566a97e06 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -11,25 +11,16 @@ public class ChunkJobCollector { private final Semaphore semaphore = new Semaphore(0); private final Consumer> collector; private final List submitted = new ArrayList<>(); - private int submittedHighEffort = 0; - private int submittedLowEffort = 0; - private final int highEffortBudget; - private final int lowEffortBudget; - private final boolean unlimitedBudget; + private long duration; public ChunkJobCollector(Consumer> collector) { - this.unlimitedBudget = true; - this.highEffortBudget = 0; - this.lowEffortBudget = 0; + this.duration = Long.MAX_VALUE; this.collector = collector; } - public ChunkJobCollector(int highEffortBudget, int lowEffortBudget, - Consumer> collector) { - this.unlimitedBudget = false; - this.highEffortBudget = highEffortBudget; - this.lowEffortBudget = lowEffortBudget; + public ChunkJobCollector(long duration, Consumer> collector) { + this.duration = duration; this.collector = collector; } @@ -56,28 +47,14 @@ public void awaitCompletion(ChunkBuilder builder) { public void addSubmittedJob(ChunkJob job) { this.submitted.add(job); + this.duration -= job.getEstimatedDuration(); + } - if (this.unlimitedBudget) { - return; - } - var effort = job.getEffort(); - if (effort <= ChunkBuilder.LOW_EFFORT) { - this.submittedLowEffort += effort; - } else { - this.submittedHighEffort += effort; - } + public boolean hasBudgetRemaining() { + return this.duration > 0; } - public boolean hasBudgetFor(int effort, boolean ignoreEffortCategory) { - if (this.unlimitedBudget) { - return true; - } - if (ignoreEffortCategory) { - return this.submittedLowEffort + this.submittedHighEffort + effort - <= this.highEffortBudget + this.lowEffortBudget; - } - return effort <= ChunkBuilder.LOW_EFFORT - ? this.submittedLowEffort + effort <= this.lowEffortBudget - : this.submittedHighEffort + effort <= this.highEffortBudget; + public int getSubmittedTaskCount() { + return this.submitted.size(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java index 0e6c2b9aa2..373f478045 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java @@ -2,17 +2,18 @@ import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.Nullable; + import java.util.ArrayDeque; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; class ChunkJobQueue { private final ConcurrentLinkedDeque jobs = new ConcurrentLinkedDeque<>(); - private final AtomicInteger jobEffortSum = new AtomicInteger(); + private final AtomicLong jobDurationSum = new AtomicLong(); private final Semaphore semaphore = new Semaphore(0); @@ -30,7 +31,7 @@ public void add(ChunkJob job, boolean important) { } else { this.jobs.addLast(job); } - this.jobEffortSum.addAndGet(job.getEffort()); + this.jobDurationSum.addAndGet(job.getEstimatedDuration()); this.semaphore.release(1); } @@ -45,7 +46,7 @@ public ChunkJob waitForNextJob() throws InterruptedException { var job = this.getNextTask(); if (job != null) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } return job; } @@ -58,7 +59,7 @@ public boolean stealJob(ChunkJob job) { var success = this.jobs.remove(job); if (success) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } else { // If we didn't manage to actually steal the task, then we need to release the permit which we did steal this.semaphore.release(1); @@ -89,7 +90,7 @@ public Collection shutdown() { // force the worker threads to wake up and exit this.semaphore.release(Runtime.getRuntime().availableProcessors()); - this.jobEffortSum.set(0); + this.jobDurationSum.set(0); return list; } @@ -98,8 +99,8 @@ public int size() { return this.semaphore.availablePermits(); } - public int getEffortSum() { - return this.jobEffortSum.get(); + public long getJobDurationSum() { + return this.jobDurationSum.get(); } public boolean isEmpty() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java index 5619855494..2a1b98ac54 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java @@ -1,22 +1,29 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.minecraft.ReportedException; public class ChunkJobResult { private final OUTPUT output; private final Throwable throwable; + private final JobEffort jobEffort; - private ChunkJobResult(OUTPUT output, Throwable throwable) { + private ChunkJobResult(OUTPUT output, Throwable throwable, JobEffort jobEffort) { this.output = output; this.throwable = throwable; + this.jobEffort = jobEffort; } public static ChunkJobResult exceptionally(Throwable throwable) { - return new ChunkJobResult<>(null, throwable); + return new ChunkJobResult<>(null, throwable, null); + } + + public static ChunkJobResult successfully(OUTPUT output, JobEffort jobEffort) { + return new ChunkJobResult<>(output, null, jobEffort); } public static ChunkJobResult successfully(OUTPUT output) { - return new ChunkJobResult<>(output, null); + return new ChunkJobResult<>(output, null, null); } public OUTPUT unwrap() { @@ -29,4 +36,8 @@ public OUTPUT unwrap() { return this.output; } + + public JobEffort getJobEffort() { + return this.jobEffort; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java index 78e1242803..201918f880 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import java.util.function.Consumer; @@ -11,13 +12,15 @@ public class ChunkJobTyped, OUTPUT extends { private final TASK task; private final Consumer> consumer; + private final boolean blocking; private volatile boolean cancelled; private volatile boolean started; - ChunkJobTyped(TASK task, Consumer> consumer) { + ChunkJobTyped(TASK task, Consumer> consumer, boolean blocking) { this.task = task; this.consumer = consumer; + this.blocking = blocking; } @Override @@ -42,6 +45,7 @@ public void execute(ChunkBuildContext context) { ChunkJobResult result; try { + var start = System.nanoTime(); var output = this.task.execute(context, this); // Task was cancelled while executing @@ -49,7 +53,7 @@ public void execute(ChunkBuildContext context) { return; } - result = ChunkJobResult.successfully(output); + result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, output.getResultSize())); } catch (Throwable throwable) { result = ChunkJobResult.exceptionally(throwable); ChunkBuilder.LOGGER.error("Chunk build failed", throwable); @@ -68,7 +72,22 @@ public boolean isStarted() { } @Override - public int getEffort() { - return this.task.getEffort(); + public boolean isBlocking() { + return this.blocking; + } + + @Override + public long getEstimatedSize() { + return this.task.getEstimatedSize(); + } + + @Override + public long getEstimatedDuration() { + return this.task.getEstimatedDuration(); + } + + @Override + public long getEstimatedUploadDuration() { + return this.task.getEstimatedUploadDuration(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java index 294b2b96e4..0627f02a9c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockOcclusionCache.java @@ -8,6 +8,7 @@ import net.minecraft.core.Direction; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -45,7 +46,8 @@ public boolean shouldDrawSide(BlockState selfState, BlockGetter view, BlockPos s // Blocks can define special behavior to control whether faces are rendered. // This is mostly used by transparent blocks (Leaves, Glass, etc.) to not render interior faces between blocks // of the same type. - if (selfState.skipRendering(otherState, facing) || PlatformBlockAccess.getInstance().shouldSkipRender(view, selfState, otherState, selfPos, otherPos, facing)) { + if (selfState.skipRendering(otherState, facing) || + PlatformBlockAccess.getInstance().shouldSkipRender(view, selfState, otherState, selfPos, otherPos, facing)) { return false; } @@ -79,6 +81,86 @@ public boolean shouldDrawSide(BlockState selfState, BlockGetter view, BlockPos s return this.lookup(selfShape, otherShape); } + /** + * Checks if a face of a fluid block should be rendered. It takes into account both occluding fluid face against + * its own waterlogged block state and the neighboring block state. This is an approximation that doesn't check + * voxel for shapes between the fluid and the neighboring block since that is handled by the fluid renderer + * separately and more accurately using actual fluid heights. It only uses voxel shape comparison for checking + * self-occlusion with the waterlogged block state. + * + * @param selfBlockState The state of the block in the level + * @param view The block view for this render context + * @param selfPos The position of the fluid + * @param facing The facing direction of the side to check + * @param fluid The fluid state + * @param fluidShape The non-empty shape of the fluid + * @return True if the fluid side facing {@param dir} is not occluded, otherwise false + */ + public boolean shouldDrawFullBlockFluidSide( + BlockState selfBlockState, + BlockGetter view, + BlockPos selfPos, + Direction facing, + FluidState fluid, + VoxelShape fluidShape) + { + var fluidShapeIsBlock = fluidShape == Shapes.block(); + + // only perform self-occlusion if the own block state can't occlude + if (selfBlockState.canOcclude()) { + var selfShape = selfBlockState.getFaceOcclusionShape(view, selfPos, facing); + + // only a non-empty self-shape can occlude anything + if (!selfShape.isEmpty()) { + // a full self-shape occludes everything + if (selfShape == Shapes.block() && fluidShapeIsBlock) { + return false; + } + + // perform occlusion of the fluid by the block it's contained in + if (!this.lookup(fluidShape, selfShape)) { + return false; + } + } + } + + // perform occlusion against the neighboring block + BlockPos.MutableBlockPos otherPos = this.cachedPositionObject; + otherPos.set(selfPos.getX() + facing.getStepX(), selfPos.getY() + facing.getStepY(), selfPos.getZ() + facing.getStepZ()); + BlockState otherState = view.getBlockState(otherPos); + + // don't render anything if the other blocks is the same fluid + if (otherState.getFluidState() == fluid) { + return false; + } + + // check for special fluid occlusion behavior + if (PlatformBlockAccess.getInstance().shouldOccludeFluid(facing.getOpposite(), otherState, fluid)) { + return false; + } + + // the up direction doesn't do occlusion with other block shapes + if (facing == Direction.UP) { + return true; + } + + // only occlude against blocks that can potentially occlude in the first place + if (!otherState.canOcclude()) { + return true; + } + + var otherShape = otherState.getFaceOcclusionShape(view, otherPos, facing.getOpposite()); + + // If the other block has an empty cull shape, then it cannot hide any geometry + if (otherShape.isEmpty()) { + return true; + } + + // If both blocks use a full-cube cull shape, then they will always hide the faces between each other. + // No voxel shape comparison is done after this point because it's redundant with the later more accurate check. + return otherShape != Shapes.block() || !fluidShapeIsBlock; + } + private boolean lookup(VoxelShape self, VoxelShape other) { ShapeComparison comparison = this.cachedComparisonObject; comparison.self = self; @@ -106,6 +188,14 @@ private boolean calculate(ShapeComparison comparison) { return result; } + private static boolean isFullShape(VoxelShape selfShape) { + return selfShape == Shapes.block(); + } + + private static boolean isEmptyShape(VoxelShape voxelShape) { + return voxelShape == Shapes.empty() || voxelShape.isEmpty(); + } + private static final class ShapeComparison { private VoxelShape self, other; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java index 8b0eac2e04..71055e2130 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.util.ColorARGB; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; @@ -20,7 +21,6 @@ import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; -import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.caffeinemc.mods.sodium.client.render.frapi.mesh.MutableQuadViewImpl; import net.caffeinemc.mods.sodium.client.render.frapi.render.AbstractBlockRenderContext; import net.caffeinemc.mods.sodium.client.render.texture.SpriteFinderCache; @@ -43,6 +43,8 @@ import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; +import java.util.Iterator; + public class BlockRenderer extends AbstractBlockRenderContext { private final ColorProviderRegistry colorProviderRegistry; private final int[] vertexColors = new int[4]; @@ -55,6 +57,7 @@ public class BlockRenderer extends AbstractBlockRenderContext { @Nullable private ColorProvider colorProvider; private TranslucentGeometryCollector collector; + private boolean allowDowngrade; public BlockRenderer(ColorProviderRegistry colorRegistry, LightPipelineProvider lighters) { this.colorProviderRegistry = colorRegistry; @@ -91,22 +94,33 @@ public void renderModel(BakedModel model, BlockState state, BlockPos pos, BlockP this.colorProvider = this.colorProviderRegistry.getColorProvider(state.getBlock()); - type = ItemBlockRenderTypes.getChunkRenderType(state); + this.type = ItemBlockRenderTypes.getChunkRenderType(state); this.prepareCulling(true); this.prepareAoInfo(model.useAmbientOcclusion()); - modelData = PlatformModelAccess.getInstance().getModelData(slice, model, state, pos, slice.getPlatformModelData(pos)); + this.modelData = PlatformModelAccess.getInstance().getModelData(this.slice, model, state, pos, this.slice.getPlatformModelData(pos)); + + Iterable renderTypes = PlatformModelAccess.getInstance().getModelRenderTypes(this.level, model, state, pos, this.random, this.modelData); + this.allowDowngrade = true; + + Iterator it = renderTypes.iterator(); + var defaultType = ItemBlockRenderTypes.getChunkRenderType(state); - Iterable renderTypes = PlatformModelAccess.getInstance().getModelRenderTypes(level, model, state, pos, random, modelData); + while (it.hasNext()) { + this.type = it.next(); + + // TODO: This can be removed once we have a better solution for https://github.com/CaffeineMC/sodium/issues/2868 + // If the model contains any materials that are not the default, we can't allow the block to be downgraded. This avoids a potentially incorrect render order if there are overlapping quads. + if (it.hasNext() || this.type != defaultType) { + this.allowDowngrade = false; + } - for (RenderType type : renderTypes) { - this.type = type; ((FabricBakedModel) model).emitBlockQuads(this.level, state, pos, this.randomSupplier, this); } - type = null; - modelData = SodiumModelData.EMPTY; + this.type = null; + this.modelData = SodiumModelData.EMPTY; } /** @@ -130,9 +144,9 @@ protected void processQuad(MutableQuadViewImpl quad) { final BlendMode blendMode = mat.blendMode(); if (blendMode == BlendMode.DEFAULT) { - material = DefaultMaterials.forRenderLayer(type); + material = DefaultMaterials.forRenderLayer(this.type); } else { - material = DefaultMaterials.forRenderLayer(blendMode.blockRenderLayer == null ? type : blendMode.blockRenderLayer); + material = DefaultMaterials.forRenderLayer(blendMode.blockRenderLayer == null ? this.type : blendMode.blockRenderLayer); } this.colorizeQuad(quad, colorIndex); @@ -146,10 +160,10 @@ private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { if (colorProvider != null) { int[] vertexColors = this.vertexColors; - colorProvider.getColors(this.slice, this.pos, this.scratchPos, this.state, quad, vertexColors); + colorProvider.getColors(this.slice, this.pos, this.scratchPos, this.state, quad, vertexColors, this.slice.hasBiomeBlend()); for (int i = 0; i < 4; i++) { - quad.color(i, ColorHelper.multiplyColor(vertexColors[i], quad.color(i))); + quad.color(i, ColorMixer.mulComponentWise(vertexColors[i], quad.color(i))); } } } @@ -186,14 +200,16 @@ private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material // attempt render pass downgrade if possible var pass = material.pass; - var downgradedPass = attemptPassDowngrade(atlasSprite, pass); + var downgradedPass = this.attemptPassDowngrade(atlasSprite, pass); if (downgradedPass != null) { pass = downgradedPass; } - // collect all translucent quads into the translucency sorting system if enabled - if (pass.isTranslucent() && this.collector != null) { - this.collector.appendQuad(quad.getFaceNormal(), vertices, normalFace); + // collect all translucent quads into the translucency sorting system if enabled, + // and discard the quad if it's invalid (i.e. not visible) + if (pass.isTranslucent() && this.collector != null && + this.collector.appendQuad(vertices, normalFace, quad.getFaceNormal())) { + return; } // if there was a downgrade from translucent to cutout, the material bits' alpha cutoff needs to be updated @@ -206,7 +222,9 @@ private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material ChunkMeshBufferBuilder vertexBuffer = builder.getVertexBuffer(normalFace); vertexBuffer.push(vertices, materialBits); - builder.addSprite(atlasSprite); + if (atlasSprite != null) { + builder.addSprite(atlasSprite); + } } private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) { @@ -228,7 +246,7 @@ private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) { } private @Nullable TerrainRenderPass attemptPassDowngrade(TextureAtlasSprite sprite, TerrainRenderPass pass) { - if (Workarounds.isWorkaroundEnabled(Workarounds.Reference.INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE)) { + if (!this.allowDowngrade || Workarounds.isWorkaroundEnabled(Workarounds.Reference.INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE)) { return null; } @@ -245,7 +263,7 @@ private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) { } if (attemptDowngrade) { - attemptDowngrade = validateQuadUVs(sprite); + attemptDowngrade = this.validateQuadUVs(sprite); } if (attemptDowngrade) { @@ -256,14 +274,23 @@ private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) { } private static TerrainRenderPass getDowngradedPass(TextureAtlasSprite sprite, TerrainRenderPass pass) { - if (sprite.contents() instanceof SpriteContentsExtension contents) { - if (pass == DefaultTerrainRenderPasses.TRANSLUCENT && !contents.sodium$hasTranslucentPixels()) { - pass = DefaultTerrainRenderPasses.CUTOUT; + if (sprite instanceof TextureAtlasSpriteExtension spriteExt) { + // Some mods may use a custom ticker which we cannot look into. To avoid problems with these mods, + // do not attempt to downgrade the render pass. + if (spriteExt.sodium$hasUnknownImageContents()) { + return pass; } - if (pass == DefaultTerrainRenderPasses.CUTOUT && !contents.sodium$hasTransparentPixels()) { - pass = DefaultTerrainRenderPasses.SOLID; + + if (sprite.contents() instanceof SpriteContentsExtension contentsExt) { + if (pass == DefaultTerrainRenderPasses.TRANSLUCENT && !contentsExt.sodium$hasTranslucentPixels()) { + pass = DefaultTerrainRenderPasses.CUTOUT; + } + if (pass == DefaultTerrainRenderPasses.CUTOUT && !contentsExt.sodium$hasTransparentPixels()) { + pass = DefaultTerrainRenderPasses.SOLID; + } } } + return pass; } } \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java index b49cde9421..249378d4f0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/DefaultFluidRenderer.java @@ -4,7 +4,6 @@ import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.api.util.NormI8; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; -import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; import net.caffeinemc.mods.sodium.client.model.light.LightMode; import net.caffeinemc.mods.sodium.client.model.light.LightPipeline; import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; @@ -28,8 +27,6 @@ import net.minecraft.tags.FluidTags; import net.minecraft.util.Mth; import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.block.LiquidBlock; -import net.minecraft.world.level.block.SupportType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.FluidState; @@ -40,7 +37,7 @@ import org.apache.commons.lang3.mutable.MutableInt; public class DefaultFluidRenderer { - // TODO: allow this to be changed by vertex format, WARNING: make sure TranslucentGeometryCollector knows about EPSILON + // TODO: allow this to be changed by vertex format, WARNING: make sure TQuad knows about EPSILON // TODO: move fluid rendering to a separate render pass and control glPolygonOffset and glDepthFunc to fix this properly public static final float EPSILON = 0.001f; private static final float ALIGNED_EQUALS_EPSILON = 0.011f; @@ -49,6 +46,8 @@ public class DefaultFluidRenderer { private final MutableFloat scratchHeight = new MutableFloat(0); private final MutableInt scratchSamples = new MutableInt(); + private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); + private final ModelQuadViewMutable quad = new ModelQuad(); private final LightPipelineProvider lighters; @@ -65,21 +64,10 @@ public DefaultFluidRenderer(LightPipelineProvider lighters) { this.lighters = lighters; } - private boolean isFluidOccluded(BlockAndTintGetter world, int x, int y, int z, Direction dir, BlockState blockState, Fluid fluid) { - //Test own block state first, this prevents waterlogged blocks from having hidden internal geometry - // which can result in z-fighting - var pos = this.scratchPos.set(x, y, z); - if (blockState.canOcclude() && blockState.isFaceSturdy(world, pos, dir, SupportType.FULL)) { - return true; - } - - //Test neighboring block state - var adjPos = this.scratchPos.set(x + dir.getStepX(), y + dir.getStepY(), z + dir.getStepZ()); - BlockState adjBlockState = world.getBlockState(adjPos); - if (adjBlockState.getFluidState().getType().isSame(fluid)) { - return true; - } - return adjBlockState.canOcclude() && dir != Direction.UP && adjBlockState.isFaceSturdy(world, adjPos, dir.getOpposite(), SupportType.FULL); + private boolean isFullBlockFluidOccluded(BlockAndTintGetter world, BlockPos pos, Direction dir, BlockState blockState, FluidState fluid) { + // check if this face of the fluid, assuming a full-block cull shape, is occluded by the block it's in or a neighboring block. + // it doesn't do a voxel shape comparison with the neighboring blocks since that is already done by isSideExposed + return !this.occlusionCache.shouldDrawFullBlockFluidSide(blockState, world, pos, dir, fluid, Shapes.block()); } private boolean isSideExposed(BlockAndTintGetter world, int x, int y, int z, Direction dir, float height) { @@ -109,15 +97,16 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat Fluid fluid = fluidState.getType(); - boolean sfUp = this.isFluidOccluded(level, posX, posY, posZ, Direction.UP, blockState, fluid); - boolean sfDown = this.isFluidOccluded(level, posX, posY, posZ, Direction.DOWN, blockState, fluid) || + boolean cullUp = this.isFullBlockFluidOccluded(level, blockPos, Direction.UP, blockState, fluidState); + boolean cullDown = this.isFullBlockFluidOccluded(level, blockPos, Direction.DOWN, blockState, fluidState) || !this.isSideExposed(level, posX, posY, posZ, Direction.DOWN, 0.8888889F); - boolean sfNorth = this.isFluidOccluded(level, posX, posY, posZ, Direction.NORTH, blockState, fluid); - boolean sfSouth = this.isFluidOccluded(level, posX, posY, posZ, Direction.SOUTH, blockState, fluid); - boolean sfWest = this.isFluidOccluded(level, posX, posY, posZ, Direction.WEST, blockState, fluid); - boolean sfEast = this.isFluidOccluded(level, posX, posY, posZ, Direction.EAST, blockState, fluid); + boolean cullNorth = this.isFullBlockFluidOccluded(level, blockPos, Direction.NORTH, blockState, fluidState); + boolean cullSouth = this.isFullBlockFluidOccluded(level, blockPos, Direction.SOUTH, blockState, fluidState); + boolean cullWest = this.isFullBlockFluidOccluded(level, blockPos, Direction.WEST, blockState, fluidState); + boolean cullEast = this.isFullBlockFluidOccluded(level, blockPos, Direction.EAST, blockState, fluidState); - if (sfUp && sfDown && sfEast && sfWest && sfNorth && sfSouth) { + // stop rendering if all faces of the fluid are occluded + if (cullUp && cullDown && cullEast && cullWest && cullNorth && cullSouth) { return; } @@ -149,7 +138,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat .move(Direction.NORTH) .move(Direction.EAST)); } - float yOffset = sfDown ? 0.0F : EPSILON; + float yOffset = cullDown ? 0.0F : EPSILON; final ModelQuadViewMutable quad = this.quad; @@ -158,7 +147,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat quad.setFlags(0); - if (!sfUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { + if (!cullUp && this.isSideExposed(level, posX, posY, posZ, Direction.UP, Math.min(Math.min(northWestHeight, southWestHeight), Math.min(southEastHeight, northEastHeight)))) { northWestHeight -= EPSILON; southWestHeight -= EPSILON; southEastHeight -= EPSILON; @@ -243,7 +232,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) } } - if (!sfDown) { + if (!cullDown) { TextureAtlasSprite sprite = sprites[0]; float minU = sprite.getU0(); @@ -273,7 +262,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) switch (dir) { case NORTH -> { - if (sfNorth) { + if (cullNorth) { continue; } c1 = northWestHeight; @@ -284,7 +273,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case SOUTH -> { - if (sfSouth) { + if (cullSouth) { continue; } c1 = southEastHeight; @@ -295,7 +284,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = z1; } case WEST -> { - if (sfWest) { + if (cullWest) { continue; } c1 = southWestHeight; @@ -306,7 +295,7 @@ && isAlignedEquals(southEastHeight, southWestHeight) z2 = 0.0f; } case EAST -> { - if (sfEast) { + if (cullEast) { continue; } c1 = northEastHeight; @@ -388,7 +377,7 @@ private void updateQuad(ModelQuadViewMutable quad, LevelSlice level, BlockPos po lighter.calculate(quad, pos, light, null, dir, false, false); - colorProvider.getColors(level, pos, scratchPos, fluidState, quad, this.quadColors); + colorProvider.getColors(level, pos, this.scratchPos, fluidState, quad, this.quadColors, level.hasBiomeBlend()); // multiply the per-vertex color against the combined brightness // the combined brightness is the per-vertex brightness multiplied by the block's brightness @@ -435,7 +424,10 @@ private void writeQuad(ChunkModelBuilder builder, TranslucentGeometryCollector c normal = NormI8.flipPacked(normal); } - collector.appendQuad(normal, vertices, facing); + // discard the quad if it's invalid (i.e. not visible) + if (collector.appendQuad(vertices, facing, normal)) { + return; + } } var vertexBuffer = builder.getVertexBuffer(facing); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/FluidRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/FluidRenderer.java index 5a9a16766d..b28456aa08 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/FluidRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/FluidRenderer.java @@ -1,7 +1,5 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; -import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; -import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.world.LevelSlice; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/TextureAtlasSpriteExtension.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/TextureAtlasSpriteExtension.java new file mode 100644 index 0000000000..462de88a24 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/TextureAtlasSpriteExtension.java @@ -0,0 +1,5 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; + +public interface TextureAtlasSpriteExtension { + boolean sodium$hasUnknownImageContents(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java index f3992c935a..d6e167744a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java @@ -1,13 +1,13 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.render.chunk.DefaultChunkRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.ExtendedBlockEntityType; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderCache; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; @@ -18,6 +18,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.PresentTranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks; @@ -43,16 +44,20 @@ /** * 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 level 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 ChunkBuilderMeshingTask extends ChunkBuilderTask { private final ChunkRenderContext renderContext; + private final SortBehavior sortBehavior; + private final boolean forceSort; - public ChunkBuilderMeshingTask(RenderSection render, int buildTime, Vector3dc absoluteCameraPos, ChunkRenderContext renderContext) { + public ChunkBuilderMeshingTask(RenderSection render, int buildTime, Vector3dc absoluteCameraPos, ChunkRenderContext renderContext, SortBehavior sortBehavior, boolean forceSort) { super(render, buildTime, absoluteCameraPos); this.renderContext = renderContext; + this.sortBehavior = sortBehavior; + this.forceSort = forceSort; } @Override @@ -80,9 +85,10 @@ public ChunkBuildOutput execute(ChunkBuildContext buildContext, CancellationToke BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(minX, minY, minZ); BlockPos.MutableBlockPos modelOffset = new BlockPos.MutableBlockPos(); + boolean sortEnabled = this.sortBehavior != SortBehavior.OFF; TranslucentGeometryCollector collector; - if (SodiumClientMod.options().performance.getSortBehavior() != SortBehavior.OFF) { - collector = new TranslucentGeometryCollector(render.getPosition()); + if (sortEnabled) { + collector = new TranslucentGeometryCollector(this.render.getPosition(), this.sortBehavior); } else { collector = null; } @@ -138,28 +144,69 @@ public ChunkBuildOutput execute(ChunkBuildContext buildContext, CancellationToke } } catch (ReportedException ex) { // Propagate existing crashes (add context) - throw fillCrashInfo(ex.getReport(), slice, blockPos); + throw this.fillCrashInfo(ex.getReport(), slice, blockPos); } catch (Exception ex) { // Create a new crash report for other exceptions (e.g. thrown in getQuads) - throw fillCrashInfo(CrashReport.forThrowable(ex, "Encountered exception while building chunk meshes"), slice, blockPos); + throw this.fillCrashInfo(CrashReport.forThrowable(ex, "Encountered exception while building chunk meshes"), slice, blockPos); } - PlatformLevelRenderHooks.INSTANCE.runChunkMeshAppenders(renderContext.getRenderers(), type -> buffers.get(DefaultMaterials.forRenderLayer(type)).asFallbackVertexConsumer(DefaultMaterials.forRenderLayer(type), collector), + PlatformLevelRenderHooks.INSTANCE.runChunkMeshAppenders(this.renderContext.getRenderers(), type -> buffers.get(DefaultMaterials.forRenderLayer(type)).asFallbackVertexConsumer(DefaultMaterials.forRenderLayer(type), collector), slice); blockRenderer.release(); SortType sortType = SortType.NONE; - if (collector != null) { + if (sortEnabled) { sortType = collector.finishRendering(); } + // cancellation opportunity right before translucent sorting + if (cancellationToken.isCancelled()) { + return null; + } + + boolean reuseUploadedData = false; + TranslucentData translucentData = null; + if (sortEnabled) { + TranslucentData oldData = this.render.getTranslucentData(); + + // Reusing non-dynamic data leads to attempting to sort with it again, + // which throws an exception since it can only generate a sorter once. + // To prevent this, reusing data is prevented when forceSort is enabled and the data is not dynamic. + if (this.forceSort && !(oldData instanceof DynamicData)) { + oldData = null; + } + + try { + translucentData = collector.getTranslucentData(oldData, this); + } catch (Exception ex) { + // Create a new crash report for exceptions thrown during sort preparation + throw this.fillCrashInfo(CrashReport.forThrowable(ex, "Encountered exception while preparing for translucency sorting"), slice, null); + } + reuseUploadedData = !this.forceSort && translucentData == oldData; + } + Map meshes = new Reference2ReferenceOpenHashMap<>(); + var visibleSlices = DefaultChunkRenderer.getVisibleFaces( + (int) this.absoluteCameraPos.x(), (int) this.absoluteCameraPos.y(), (int) this.absoluteCameraPos.z(), + this.render.getChunkX(), this.render.getChunkY(), this.render.getChunkZ()); + + if (translucentData != null && translucentData.meshesWereModified()) { + meshes.put(DefaultTerrainRenderPasses.TRANSLUCENT, buffers.createModifiedTranslucentMesh(translucentData.getUpdatedQuads())); + renderData.addRenderPass(DefaultTerrainRenderPasses.TRANSLUCENT); + } for (TerrainRenderPass pass : DefaultTerrainRenderPasses.ALL) { - // consolidate all translucent geometry into UNASSIGNED so that it's rendered - // all together if it needs to share an index buffer between the directions - BuiltSectionMeshParts mesh = buffers.createMesh(pass, pass.isTranslucent() && sortType.needsDirectionMixing); + if (meshes.containsKey(pass)) { + continue; + } + + // if the translucent geometry needs to share an index buffer between the directions, + // consolidate all translucent geometry into UNASSIGNED + boolean translucentBehavior = sortEnabled && pass.isTranslucent(); + boolean forceUnassigned = translucentBehavior && sortType.needsDirectionMixing; + boolean sliceReordering = !translucentBehavior || sortType.allowSliceReordering; + BuiltSectionMeshParts mesh = buffers.createMesh(pass, visibleSlices, forceUnassigned, sliceReordering); if (mesh != null) { meshes.put(pass, mesh); @@ -167,31 +214,20 @@ public ChunkBuildOutput execute(ChunkBuildContext buildContext, CancellationToke } } - // cancellation opportunity right before translucent sorting - if (cancellationToken.isCancelled()) { - meshes.forEach((pass, mesh) -> mesh.getVertexData().free()); - return null; - } - renderData.setOcclusionData(occluder.resolve()); - - boolean reuseUploadedData = false; - TranslucentData translucentData = null; - if (collector != null) { - var oldData = this.render.getTranslucentData(); - translucentData = collector.getTranslucentData( - oldData, meshes.get(DefaultTerrainRenderPasses.TRANSLUCENT), this); - reuseUploadedData = translucentData == oldData; - } - var output = new ChunkBuildOutput(this.render, this.submitTime, translucentData, renderData.build(), meshes); - if (collector != null) { + if (sortEnabled) { if (reuseUploadedData) { output.markAsReusingUploadedData(); } else if (translucentData instanceof PresentTranslucentData present) { - var sorter = present.getSorter(); - sorter.writeIndexBuffer(this, true); - output.copyResultFrom(sorter); + try { + var sorter = present.getSorter(); + sorter.writeIndexBuffer(this, true); + output.setSorter(sorter); + } catch (Exception ex) { + // Create a new crash report for exceptions thrown during sorting + throw this.fillCrashInfo(CrashReport.forThrowable(ex, "Encountered exception while writing index buffer for translucent geometry"), slice, null); + } } } @@ -201,11 +237,14 @@ public ChunkBuildOutput execute(ChunkBuildContext buildContext, CancellationToke private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, BlockPos pos) { CrashReportCategory crashReportSection = report.addCategory("Block being rendered", 1); - BlockState state = null; - try { - state = slice.getBlockState(pos); - } catch (Exception ignored) {} - CrashReportCategory.populateBlockDetails(crashReportSection, slice, pos, state); + if (pos != null) { + BlockState state = null; + try { + state = slice.getBlockState(pos); + } catch (Exception ignored) { + } + CrashReportCategory.populateBlockDetails(crashReportSection, slice, pos, state); + } crashReportSection.setDetail("Chunk section", this.render); if (this.renderContext != null) { @@ -216,7 +255,7 @@ private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, Bl } @Override - public int getEffort() { - return ChunkBuilder.HIGH_EFFORT; + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return estimator.estimateSize(this.render); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java index c4efd4ddc1..ae58d4e33d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java @@ -1,19 +1,18 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.Sorter; -import org.joml.Vector3dc; - import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicSorter; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; +import org.joml.Vector3dc; public class ChunkBuilderSortingTask extends ChunkBuilderTask { - private final Sorter sorter; + private final DynamicSorter sorter; - public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, Sorter sorter) { + public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, DynamicSorter sorter) { super(render, frame, absoluteCameraPos); this.sorter = sorter; } @@ -35,7 +34,7 @@ public static ChunkBuilderSortingTask createTask(RenderSection render, int frame } @Override - public int getEffort() { - return ChunkBuilder.LOW_EFFORT; + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return this.sorter.getResultSize(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java index 1735c23bb6..fee3395b5a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java @@ -1,22 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import org.joml.Vector3dc; -import org.joml.Vector3f; -import org.joml.Vector3fc; - import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.UploadDurationEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.CombinedCameraPos; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; +import org.joml.Vector3dc; +import org.joml.Vector3f; +import org.joml.Vector3fc; /** * 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. */ @@ -26,6 +28,10 @@ public abstract class ChunkBuilderTask impleme protected final Vector3dc absoluteCameraPos; protected final Vector3fc cameraPos; + private long estimatedSize; + private long estimatedDuration; + private long estimatedUploadDuration; + /** * Constructs a new build task for the given chunk and converts the absolute camera position to a relative position. While the absolute position is stored as a double vector, the relative position is stored as a float vector. * @@ -54,7 +60,25 @@ public ChunkBuilderTask(RenderSection render, int time, Vector3dc absoluteCamera */ public abstract OUTPUT execute(ChunkBuildContext context, CancellationToken cancellationToken); - public abstract int getEffort(); + public abstract long estimateTaskSizeWith(MeshTaskSizeEstimator estimator); + + public void calculateEstimations(JobDurationEstimator jobEstimator, MeshTaskSizeEstimator sizeEstimator, UploadDurationEstimator uploadEstimator) { + this.estimatedSize = this.estimateTaskSizeWith(sizeEstimator); + this.estimatedDuration = jobEstimator.estimateJobDuration(this.getClass(), this.estimatedSize); + this.estimatedUploadDuration = uploadEstimator.estimateUploadDuration(this.estimatedSize); + } + + public long getEstimatedSize() { + return this.estimatedSize; + } + + public long getEstimatedDuration() { + return this.estimatedDuration; + } + + public long getEstimatedUploadDuration() { + return this.estimatedUploadDuration; + } @Override public Vector3fc getRelativeCameraPos() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java index 25bf883a4d..744e06224d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java @@ -1,10 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.data; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.VisibilityEncoding; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.minecraft.client.renderer.chunk.VisibilitySet; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.Direction; @@ -78,8 +78,8 @@ public void setOcclusionData(VisibilitySet data) { * before rendering as necessary. * @param sprite The sprite */ - public void addSprite(TextureAtlasSprite sprite) { - if (SpriteUtil.hasAnimation(sprite)) { + public void addSprite(@NotNull TextureAtlasSprite sprite) { + if (SpriteUtil.INSTANCE.hasAnimation(sprite)) { this.animatedSprites.add(sprite); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionMeshParts.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionMeshParts.java index ec250afb86..ae98d13a35 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionMeshParts.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionMeshParts.java @@ -1,13 +1,22 @@ package net.caffeinemc.mods.sodium.client.render.chunk.data; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; +/** + * The array of vertex segments is structured as follows: + * - It consists of 2 * ModelQuadFacing.COUNT ints. + * - The first and every second int after that are vertex counts. + * - The second and every second int after that are the ModelQuadFacing index that the preceding count applies to. + * - If the vertex count is zero, the segment is not used and reading the facing index is undefined behavior. + * - The array of vertex segments starts with some number of filled segments, followed by empty segments for the rest of the fixed size. + */ public class BuiltSectionMeshParts { - private final int[] vertexCounts; + private final int[] vertexSegments; private final NativeBuffer buffer; - public BuiltSectionMeshParts(NativeBuffer buffer, int[] vertexCounts) { - this.vertexCounts = vertexCounts; + public BuiltSectionMeshParts(NativeBuffer buffer, int[] vertexSegments) { + this.vertexSegments = vertexSegments; this.buffer = buffer; } @@ -15,7 +24,21 @@ public NativeBuffer getVertexData() { return this.buffer; } - public int[] getVertexCounts() { - return this.vertexCounts; + public int[] getVertexSegments() { + return this.vertexSegments; + } + + public int[] computeVertexCounts() { + var vertexCounts = new int[ModelQuadFacing.COUNT]; + + for (int i = 0; i < this.vertexSegments.length; i += 2) { + var vertexCount = this.vertexSegments[i]; + if (vertexCount == 0) { + continue; // Skip non-present segments + } + vertexCounts[this.vertexSegments[i + 1]] = vertexCount; + } + + return vertexCounts; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java index bcc184f714..68c01fc829 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java @@ -1,13 +1,18 @@ package net.caffeinemc.mods.sodium.client.render.chunk.data; +import net.caffeinemc.mods.sodium.client.gl.arena.GlBufferArena; import net.caffeinemc.mods.sodium.client.gl.arena.GlBufferSegment; +import net.caffeinemc.mods.sodium.client.gl.arena.PendingUpload; +import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.render.chunk.SharedQuadIndexBuffer; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.util.UInt32; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Arrays; +import java.util.stream.Stream; /** * The section render data storage stores the gl buffer segments of uploaded @@ -17,20 +22,24 @@ * buffer segments is stored in a natively allocated piece of memory referenced * by {@code pMeshDataArray} and accessed through * {@link SectionRenderDataUnsafe}. - * + *

* When the backing buffer (from the gl buffer arena) is resized, the storage - * object is notified and then it updates the changed offsets of the buffer + * object is notified, and then it updates the changed offsets of the buffer * segments. Since the index data's size and alignment directly corresponds to * that of the vertex data except for the vertex/index scaling of two thirds, * only an offset to the index data within the index data buffer arena is * stored. - * + *

* Index and vertex data storage can be managed separately since they may be * updated independently of each other (in both directions). */ public class SectionRenderDataStorage { private final @Nullable GlBufferSegment[] vertexAllocations; - private final @Nullable GlBufferSegment @Nullable[] elementAllocations; + private final @Nullable GlBufferSegment @Nullable [] elementAllocations; + private @Nullable GlBufferSegment sharedIndexAllocation; + private int sharedIndexCapacity = 0; + private boolean needsSharedIndexUpdate = false; + private final int[] sharedIndexUsage = new int[RenderRegion.REGION_SIZE]; private final long pMeshDataArray; @@ -46,8 +55,7 @@ public SectionRenderDataStorage(boolean storesIndices) { this.pMeshDataArray = SectionRenderDataUnsafe.allocateHeap(RenderRegion.REGION_SIZE); } - public void setVertexData(int localSectionIndex, - GlBufferSegment allocation, int[] vertexCounts) { + public void setVertexData(int localSectionIndex, GlBufferSegment allocation, int[] vertexSegments) { GlBufferSegment prev = this.vertexAllocations[localSectionIndex]; if (prev != null) { @@ -59,30 +67,30 @@ public void setVertexData(int localSectionIndex, var pMeshData = this.getDataPointer(localSectionIndex); int sliceMask = 0; + long facingList = 0; - long vertexOffset = allocation.getOffset(); + for (int i = 0; i < ModelQuadFacing.COUNT; i++) { + var segmentIndex = i << 1; - for (int facingIndex = 0; facingIndex < ModelQuadFacing.COUNT; facingIndex++) { - long vertexCount = vertexCounts[facingIndex]; + int facing = vertexSegments[segmentIndex + 1]; + facingList |= (long) facing << (i * 8); - SectionRenderDataUnsafe.setVertexOffset(pMeshData, facingIndex, - UInt32.downcast(vertexOffset)); - SectionRenderDataUnsafe.setElementCount(pMeshData, facingIndex, - UInt32.downcast((vertexCount >> 2) * 6)); + long vertexCount = UInt32.upcast(vertexSegments[segmentIndex]); + SectionRenderDataUnsafe.setVertexCount(pMeshData, i, vertexCount); if (vertexCount > 0) { - sliceMask |= 1 << facingIndex; + sliceMask |= 1 << facing; } - - vertexOffset += vertexCount; } + SectionRenderDataUnsafe.setBaseVertex(pMeshData, allocation.getOffset()); SectionRenderDataUnsafe.setSliceMask(pMeshData, sliceMask); + SectionRenderDataUnsafe.setFacingList(pMeshData, facingList); } public void setIndexData(int localSectionIndex, GlBufferSegment allocation) { if (this.elementAllocations == null) { - throw new IllegalStateException("Cannot set index data when storesIndices is false"); + throw new IllegalStateException("Cannot set index data on a render data storage that does not store indices"); } GlBufferSegment prev = this.elementAllocations[localSectionIndex]; @@ -95,56 +103,144 @@ public void setIndexData(int localSectionIndex, GlBufferSegment allocation) { var pMeshData = this.getDataPointer(localSectionIndex); - SectionRenderDataUnsafe.setBaseElement(pMeshData, allocation.getOffset()); + SectionRenderDataUnsafe.setLocalBaseElement(pMeshData, allocation.getOffset()); } - public void removeData(int localSectionIndex) { - this.removeVertexData(localSectionIndex, false); + public boolean setSharedIndexUsage(int localSectionIndex, int newUsage) { + var previousUsage = this.sharedIndexUsage[localSectionIndex]; + if (previousUsage == newUsage) { + return false; + } - if (this.elementAllocations != null) { - this.removeIndexData(localSectionIndex); + // mark for update if usage is down from max (may need to shrink buffer) + // or if usage increased beyond the max (need to grow buffer) + boolean newlyUsingSharedIndexBuffer = false; + if (newUsage < previousUsage && previousUsage == this.sharedIndexCapacity || + newUsage > this.sharedIndexCapacity || + newUsage > 0 && this.sharedIndexAllocation == null) { + this.needsSharedIndexUpdate = true; + } else { + // just set the base element since no update is happening + var sharedBaseElement = this.sharedIndexAllocation.getOffset(); + var pMeshData = this.getDataPointer(localSectionIndex); + SectionRenderDataUnsafe.setSharedBaseElement(pMeshData, sharedBaseElement); + + if (previousUsage == 0 && newUsage > 0) { + newlyUsingSharedIndexBuffer = true; + } } + + this.sharedIndexUsage[localSectionIndex] = newUsage; + + return newlyUsingSharedIndexBuffer; } - public void removeVertexData(int localSectionIndex) { - this.removeVertexData(localSectionIndex, true); + public boolean needsSharedIndexUpdate() { + return this.needsSharedIndexUpdate; } - private void removeVertexData(int localSectionIndex, boolean retainIndexData) { - GlBufferSegment prev = this.vertexAllocations[localSectionIndex]; + /** + * Updates the shared index data buffer to match the current usage. + * + * @param arena The buffer arena to allocate the new buffer from + * @return true if the arena resized itself + */ + public boolean updateSharedIndexData(CommandList commandList, GlBufferArena arena, float regionFillFractionInv) { + // assumes this.needsSharedIndexUpdate is true when this is called + this.needsSharedIndexUpdate = false; + + // determine the new required capacity + int newCapacity = 0; + for (int i = 0; i < RenderRegion.REGION_SIZE; i++) { + newCapacity = Math.max(newCapacity, this.sharedIndexUsage[i]); + } + if (newCapacity == this.sharedIndexCapacity) { + return false; + } - if (prev == null) { - return; + this.sharedIndexCapacity = newCapacity; + + // remove the existing allocation and exit if we don't need to create a new one + if (this.sharedIndexAllocation != null) { + this.sharedIndexAllocation.delete(); + this.sharedIndexAllocation = null; + } + if (this.sharedIndexCapacity == 0) { + return false; } - prev.delete(); + // add some base-level capacity to avoid resizing the buffer too often + if (this.sharedIndexCapacity < 128) { + this.sharedIndexCapacity += 32; + } - this.vertexAllocations[localSectionIndex] = null; + // create and upload a new shared index buffer + var buffer = SharedQuadIndexBuffer.createIndexBuffer(SharedQuadIndexBuffer.IndexType.INTEGER, this.sharedIndexCapacity); + var pendingUpload = new PendingUpload(buffer); + var bufferChanged = arena.upload(commandList, Stream.of(pendingUpload), regionFillFractionInv); + this.sharedIndexAllocation = pendingUpload.getResult(); + buffer.free(); + + // only write the base elements now if we're not going to do so again later because of the buffer resize + if (!bufferChanged) { + var sharedBaseElement = this.sharedIndexAllocation.getOffset(); + for (int i = 0; i < RenderRegion.REGION_SIZE; i++) { + if (this.sharedIndexUsage[i] > 0) { + SectionRenderDataUnsafe.setSharedBaseElement(this.getDataPointer(i), sharedBaseElement); + } + } + } - var pMeshData = this.getDataPointer(localSectionIndex); + return bufferChanged; + } - var baseElement = SectionRenderDataUnsafe.getBaseElement(pMeshData); - SectionRenderDataUnsafe.clear(pMeshData); + private boolean storesIndexData() { + return this.elementAllocations != null; + } - if (retainIndexData) { - SectionRenderDataUnsafe.setBaseElement(pMeshData, baseElement); + public void removeIndexData(int localSectionIndex) { + if (!this.storesIndexData()) { + throw new IllegalStateException("Cannot remove index data on a render data storage that does not store indices"); } + this.removeData(localSectionIndex, false, true); } - public void removeIndexData(int localSectionIndex) { - final GlBufferSegment[] allocations = this.elementAllocations; + public void removeVertexData(int localSectionIndex) { + this.removeData(localSectionIndex, true, false); + } + + public void removeData(int localSectionIndex) { + this.removeData(localSectionIndex, true, true); + } - if (allocations == null) { - throw new IllegalStateException("Cannot remove index data when storesIndices is false"); + private void removeData(int localSectionIndex, boolean removeVertexData, boolean removeIndexData) { + if (removeVertexData) { + GlBufferSegment prev = this.vertexAllocations[localSectionIndex]; + if (prev != null) { + prev.delete(); + this.vertexAllocations[localSectionIndex] = null; + } } + if (removeIndexData && this.storesIndexData()) { + GlBufferSegment prev = this.elementAllocations[localSectionIndex]; - GlBufferSegment prev = allocations[localSectionIndex]; + if (prev != null) { + prev.delete(); + this.elementAllocations[localSectionIndex] = null; + } - if (prev != null) { - prev.delete(); + this.setSharedIndexUsage(localSectionIndex, 0); } - allocations[localSectionIndex] = null; + var pMeshData = this.getDataPointer(localSectionIndex); + + if ((removeIndexData || !this.storesIndexData()) && removeVertexData) { + SectionRenderDataUnsafe.clearFull(pMeshData); + } else if (removeVertexData) { + SectionRenderDataUnsafe.clearVertexData(pMeshData); + } else if (removeIndexData) { + SectionRenderDataUnsafe.clearIndexData(pMeshData); + } } public void onBufferResized() { @@ -160,27 +256,27 @@ private void updateMeshes(int sectionIndex) { return; } - long offset = allocation.getOffset(); var data = this.getDataPointer(sectionIndex); - - for (int facing = 0; facing < ModelQuadFacing.COUNT; facing++) { - SectionRenderDataUnsafe.setVertexOffset(data, facing, offset); - - var count = SectionRenderDataUnsafe.getElementCount(data, facing); - offset += (count / 6) * 4; // convert elements back into vertices - } + long offset = allocation.getOffset(); + SectionRenderDataUnsafe.setBaseVertex(data, offset); } public void onIndexBufferResized() { - if (this.elementAllocations == null) { - return; + long sharedBaseElement = 0; + if (this.sharedIndexAllocation != null) { + sharedBaseElement = this.sharedIndexAllocation.getOffset(); } - for (int sectionIndex = 0; sectionIndex < RenderRegion.REGION_SIZE; sectionIndex++) { - var allocation = this.elementAllocations[sectionIndex]; + for (int i = 0; i < RenderRegion.REGION_SIZE; i++) { + if (this.sharedIndexUsage[i] > 0) { + // update index sharing sections to use the new shared index buffer's offset + SectionRenderDataUnsafe.setSharedBaseElement(this.getDataPointer(i), sharedBaseElement); + } else if (this.elementAllocations != null) { + var allocation = this.elementAllocations[i]; - if (allocation != null) { - SectionRenderDataUnsafe.setBaseElement(this.getDataPointer(sectionIndex), allocation.getOffset()); + if (allocation != null) { + SectionRenderDataUnsafe.setLocalBaseElement(this.getDataPointer(i), allocation.getOffset()); + } } } } @@ -196,6 +292,10 @@ public void delete() { deleteAllocations(this.elementAllocations); } + if (this.sharedIndexAllocation != null) { + this.sharedIndexAllocation.delete(); + } + SectionRenderDataUnsafe.freeHeap(this.pMeshDataArray); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataUnsafe.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataUnsafe.java index 665973beff..3e79a43584 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataUnsafe.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataUnsafe.java @@ -16,31 +16,30 @@ // Please never try to write performance critical code in Java. This is what it will do to you. And you will still be // three times slower than the most naive solution in literally any other language that LLVM can compile. -// struct SectionRenderData { // 64 bytes -// base_element: u32 -// mask: u32, -// ranges: [VertexRange; 7] -// } -// -// struct VertexRange { // 8 bytes -// offset: u32, -// count: u32 +// struct SectionRenderData { // 48 bytes +// base_element: u32, +// base_vertex: u32, +// (is_local_index | facing_list): (u8 | u56), +// slice_mask: u32, +// vertex_count: [u32; 7] // } public class SectionRenderDataUnsafe { /** * When the "base element" field is not specified (indicated by setting the MSB to 0), the indices for the geometry set * should be sourced from a monotonic sequence (see {@link net.caffeinemc.mods.sodium.client.render.chunk.SharedQuadIndexBuffer}). - * + *

* Otherwise, indices should be sourced from the index buffer for the render region using the specified offset. */ private static final long OFFSET_BASE_ELEMENT = 0; + private static final long OFFSET_BASE_VERTEX = 4; + private static final long OFFSET_FACING_LIST = 8; + private static final long OFFSET_IS_LOCAL_INDEX = 15; + private static final long OFFSET_SLICE_MASK = 16; + private static final long OFFSET_ELEMENT_COUNTS = 20; - private static final long OFFSET_SLICE_MASK = 4; - private static final long OFFSET_SLICE_RANGES = 8; - - private static final long ALIGNMENT = 64; - private static final long STRIDE = 64; // cache-line friendly! :) + private static final long ALIGNMENT = 8; // 8 byte (64 bit) alignment for reading longs + private static final long STRIDE = 48; public static long allocateHeap(int count) { final var bytes = STRIDE * count; @@ -55,14 +54,45 @@ public static void freeHeap(long pointer) { MemoryUtil.nmemAlignedFree(pointer); } - public static void clear(long pointer) { + public static void clearFull(long pointer) { MemoryUtil.memSet(pointer, 0x0, STRIDE); } + public static void clearVertexData(long ptr) { + // save the base element and the local indexing flag + int baseElement = MemoryUtil.memGetInt(ptr + OFFSET_BASE_ELEMENT); + byte isLocalIndexByte = MemoryUtil.memGetByte(ptr + OFFSET_IS_LOCAL_INDEX); + + clearFull(ptr); + + // restore the base element and the local indexing flag + MemoryUtil.memPutInt(ptr + OFFSET_BASE_ELEMENT, baseElement); + MemoryUtil.memPutByte(ptr + OFFSET_IS_LOCAL_INDEX, isLocalIndexByte); + } + + public static void clearIndexData(long ptr) { + MemoryUtil.memPutInt(ptr + OFFSET_BASE_ELEMENT, 0); + MemoryUtil.memPutByte(ptr + OFFSET_IS_LOCAL_INDEX, (byte)0); + } + public static long heapPointer(long ptr, int index) { return ptr + (index * STRIDE); } + public static void setLocalBaseElement(long ptr, long value /* Uint32 */) { + MemoryUtil.memPutInt(ptr + OFFSET_BASE_ELEMENT, UInt32.downcast(value)); + MemoryUtil.memPutByte(ptr + OFFSET_IS_LOCAL_INDEX, (byte) 1); + } + + public static void setSharedBaseElement(long ptr, long value /* Uint32 */) { + MemoryUtil.memPutInt(ptr + OFFSET_BASE_ELEMENT, UInt32.downcast(value)); + MemoryUtil.memPutByte(ptr + OFFSET_IS_LOCAL_INDEX, (byte) 0); + } + + public static long getBaseElement(long ptr) { + return Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr + OFFSET_BASE_ELEMENT)); + } + public static void setSliceMask(long ptr, int value) { MemoryUtil.memPutInt(ptr + OFFSET_SLICE_MASK, value); } @@ -71,27 +101,34 @@ public static int getSliceMask(long ptr) { return MemoryUtil.memGetInt(ptr + OFFSET_SLICE_MASK); } - public static void setBaseElement(long ptr, long value) { - MemoryUtil.memPutInt(ptr + OFFSET_BASE_ELEMENT, UInt32.downcast(value)); + public static void setFacingList(long ptr, long facingList) { + // rescue and re-write the local indexing flag to prevent it from being lost when a whole long is written for the facing list + byte isLocalIndexByte = MemoryUtil.memGetByte(ptr + OFFSET_IS_LOCAL_INDEX); + MemoryUtil.memPutLong(ptr + OFFSET_FACING_LIST, facingList); + MemoryUtil.memPutByte(ptr + OFFSET_IS_LOCAL_INDEX, isLocalIndexByte); } - public static long getBaseElement(long ptr) { - return Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr + OFFSET_BASE_ELEMENT)); + public static long getFacingList(long ptr) { + return MemoryUtil.memGetLong(ptr + OFFSET_FACING_LIST); + } + + public static boolean isLocalIndex(long ptr) { + return MemoryUtil.memGetByte(ptr + OFFSET_IS_LOCAL_INDEX) != 0; } - public static void setVertexOffset(long ptr, int facing, long value /* Uint32 */) { - MemoryUtil.memPutInt(ptr + OFFSET_SLICE_RANGES + (facing * 8L) + 0L, UInt32.downcast(value)); + public static void setBaseVertex(long ptr, long value /* Uint32 */) { + MemoryUtil.memPutInt(ptr + OFFSET_BASE_VERTEX, UInt32.downcast(value)); } - public static long /* Uint32 */ getVertexOffset(long ptr, int facing) { - return UInt32.upcast(MemoryUtil.memGetInt(ptr + OFFSET_SLICE_RANGES + (facing * 8L) + 0L)); + public static long /* Uint32 */ getBaseVertex(long ptr) { + return UInt32.upcast(MemoryUtil.memGetInt(ptr + OFFSET_BASE_VERTEX)); } - public static void setElementCount(long ptr, int facing, long value /* Uint32 */) { - MemoryUtil.memPutInt(ptr + OFFSET_SLICE_RANGES + (facing * 8L) + 4L, UInt32.downcast(value)); + public static void setVertexCount(long ptr, int index, long count /* Uint32 */) { + MemoryUtil.memPutInt(ptr + OFFSET_ELEMENT_COUNTS + (index * 4), UInt32.downcast(count)); } - public static long /* Uint32 */ getElementCount(long ptr, int facing) { - return UInt32.upcast(MemoryUtil.memGetInt(ptr + OFFSET_SLICE_RANGES + (facing * 8L) + 4L)); + public static long /* Uint32 */ getVertexCount(long ptr, int index) { + return UInt32.upcast(MemoryUtil.memGetInt(ptr + OFFSET_ELEMENT_COUNTS + (index * 4))); } } \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java index 75e6757df8..426f76b9ce 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java @@ -1,18 +1,29 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; -import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.LocalSectionIndex; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator; import net.caffeinemc.mods.sodium.client.util.iterator.ByteIterator; import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleByteArrayIterator; -import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator; -import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; + public class ChunkRenderList { private final RenderRegion region; private final byte[] sectionsWithGeometry = new byte[RenderRegion.REGION_SIZE]; + private final long[] sectionsWithGeometryMap = new long[RenderRegion.REGION_SIZE / Long.SIZE]; + private final long[] prevSectionsWithGeometryMap = new long[RenderRegion.REGION_SIZE / Long.SIZE]; private int sectionsWithGeometryCount = 0; + private int prevSectionsWithGeometryCount = 0; + private int lastRelativeCameraSectionX; + private int lastRelativeCameraSectionY; + private int lastRelativeCameraSectionZ; + private boolean addedSectionsAreSorted = false; private final byte[] sectionsWithSprites = new byte[RenderRegion.REGION_SIZE]; private int sectionsWithSpritesCount = 0; @@ -28,27 +39,105 @@ public ChunkRenderList(RenderRegion region) { this.region = region; } - public void reset(int frame) { + public void reset(int frame, boolean addedSectionsAreSorted) { + this.prevSectionsWithGeometryCount = this.sectionsWithGeometryCount; + Arrays.fill(this.sectionsWithGeometryMap, 0L); + this.sectionsWithGeometryCount = 0; this.sectionsWithSpritesCount = 0; this.sectionsWithEntitiesCount = 0; this.size = 0; this.lastVisibleFrame = frame; + this.addedSectionsAreSorted = addedSectionsAreSorted; } - public void add(RenderSection render) { - if (this.size >= RenderRegion.REGION_SIZE) { - throw new ArrayIndexOutOfBoundsException("Render list is full"); + // clamping the relative camera position to the region bounds means there can only be very few different distances + private static final int SORTING_HISTOGRAM_SIZE = RenderRegion.REGION_WIDTH + RenderRegion.REGION_HEIGHT + RenderRegion.REGION_LENGTH - 2; + + public void prepareForRender(SectionPos cameraPos, SortItemsProvider sortItemsProvider) { + // The relative coordinates are clamped to one section larger than the region bounds to also capture cache invalidation that happens + // when the camera moves from outside the region to inside the region (when seen on all axes independently). + // This type of cache invalidation stems from different facings of sections being rendered if the camera is aligned with them on an axis. + // For sorting only the position clamped to inside the region is used. + int relativeCameraSectionX = Mth.clamp(cameraPos.getX() - this.region.getChunkX(), -1, RenderRegion.REGION_WIDTH); + int relativeCameraSectionY = Mth.clamp(cameraPos.getY() - this.region.getChunkY(), -1, RenderRegion.REGION_HEIGHT); + int relativeCameraSectionZ = Mth.clamp(cameraPos.getZ() - this.region.getChunkZ(), -1, RenderRegion.REGION_LENGTH); + + // invalidate batch cache if the render list changed + if (this.prevSectionsWithGeometryCount != this.sectionsWithGeometryCount || + relativeCameraSectionX != this.lastRelativeCameraSectionX || + relativeCameraSectionY != this.lastRelativeCameraSectionY || + relativeCameraSectionZ != this.lastRelativeCameraSectionZ || + !Arrays.equals(this.sectionsWithGeometryMap, this.prevSectionsWithGeometryMap)) { + // reset cache invalidation, the newly built batches will remain valid until the next change + this.region.clearAllCachedBatches(); + + this.prevSectionsWithGeometryCount = this.sectionsWithGeometryCount; + System.arraycopy(this.sectionsWithGeometryMap, 0, this.prevSectionsWithGeometryMap, 0, this.sectionsWithGeometryMap.length); + this.lastRelativeCameraSectionX = relativeCameraSectionX; + this.lastRelativeCameraSectionY = relativeCameraSectionY; + this.lastRelativeCameraSectionZ = relativeCameraSectionZ; + + // only sort sections if necessary, read directly from bitmap instead of no sorting is required + if (!this.addedSectionsAreSorted) { + this.sortSections(relativeCameraSectionX, relativeCameraSectionY, relativeCameraSectionZ, sortItemsProvider); + } } + } - this.size++; + private void sortSections(int relativeCameraSectionX, int relativeCameraSectionY, int relativeCameraSectionZ, SortItemsProvider sortItemsProvider) { + relativeCameraSectionX = Mth.clamp(relativeCameraSectionX, 0, RenderRegion.REGION_WIDTH - 1); + relativeCameraSectionY = Mth.clamp(relativeCameraSectionY, 0, RenderRegion.REGION_HEIGHT - 1); + relativeCameraSectionZ = Mth.clamp(relativeCameraSectionZ, 0, RenderRegion.REGION_LENGTH - 1); - int index = render.getSectionIndex(); - int flags = render.getFlags(); + int[] histogram = new int[SORTING_HISTOGRAM_SIZE]; + var sortItems = sortItemsProvider.ensureSortItemsOfLength(this.sectionsWithGeometryCount); - this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) index; - this.sectionsWithGeometryCount += (flags >>> RenderSectionFlags.HAS_BLOCK_GEOMETRY) & 1; + this.sectionsWithGeometryCount = 0; + for (int mapIndex = 0; mapIndex < this.sectionsWithGeometryMap.length; mapIndex++) { + var map = this.sectionsWithGeometryMap[mapIndex]; + var mapOffset = mapIndex << 6; + + while (map != 0) { + var index = Long.numberOfTrailingZeros(map) + mapOffset; + map &= map - 1; + + var x = Math.abs(LocalSectionIndex.unpackX(index) - relativeCameraSectionX); + var y = Math.abs(LocalSectionIndex.unpackY(index) - relativeCameraSectionY); + var z = Math.abs(LocalSectionIndex.unpackZ(index) - relativeCameraSectionZ); + + var distance = x + y + z; + histogram[distance]++; + sortItems[this.sectionsWithGeometryCount++] = distance << 8 | index; + } + } + + // prefix sum to calculate indexes + for (int i = 1; i < SORTING_HISTOGRAM_SIZE; i++) { + histogram[i] += histogram[i - 1]; + } + + for (int i = 0; i < this.sectionsWithGeometryCount; i++) { + var item = sortItems[i]; + var distance = item >>> 8; + this.sectionsWithGeometry[--histogram[distance]] = (byte) item; + } + } + + /* + In immediate presentation mode this method is called during the processing of results for immediate-mode built sections. Sometimes this means that their render flags change as a result of the build. However, when the section was already entered into the render list and the render list was full, this resulted in an exception since previously this method threw when the render list was full. It's also not good to have the section be in the render list twice. The method now accepts a flags parameter, which is set to only the new flags when updating the render lists with immediate built results, to prevent this issue. + */ + public void add(int index, int flags) { + this.size++; + + if (((flags >>> RenderSectionFlags.HAS_BLOCK_GEOMETRY) & 1) == 1) { + this.sectionsWithGeometryMap[index >> 6] |= 1L << (index & 0b111111); + if (this.addedSectionsAreSorted) { + this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) index; + } + this.sectionsWithGeometryCount++; + } this.sectionsWithSprites[this.sectionsWithSpritesCount] = (byte) index; this.sectionsWithSpritesCount += (flags >>> RenderSectionFlags.HAS_ANIMATED_SPRITES) & 1; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/CoordinateSectionVisitor.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/CoordinateSectionVisitor.java new file mode 100644 index 0000000000..62af2d4294 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/CoordinateSectionVisitor.java @@ -0,0 +1,5 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +public interface CoordinateSectionVisitor { + void visit(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/OcclusionSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/OcclusionSectionCollector.java new file mode 100644 index 0000000000..b627ae41ca --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/OcclusionSectionCollector.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType; + +/** + * The occlusion section collector is passed to the occlusion graph search culler to + * collect the visible chunks. + */ +public class OcclusionSectionCollector extends SectionCollector { + public OcclusionSectionCollector(int frame, TaskQueueType importantRebuildQueueType, TaskQueueType importantSortQueueType) { + super(frame, importantRebuildQueueType, importantSortQueueType); + } + + @Override + public boolean orderIsSorted() { + return false; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java new file mode 100644 index 0000000000..243fe21ef1 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java @@ -0,0 +1,59 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +import java.util.ArrayDeque; +import java.util.Map; + +public interface RenderListProvider extends SortItemsProvider { + ObjectArrayList getUnsortedRenderLists(); + + Map> getTaskLists(); + + boolean needsRevisitForPendingUpdates(); + + boolean orderIsSorted(); + + default SortedRenderLists createRenderLists(Viewport viewport) { + var sectionPos = viewport.getChunkCoord(); + var unsorted = this.getUnsortedRenderLists(); + + // sort the regions by distance to fix rare region ordering problems. + // regions need to be sorted even when the order is generated by a correctly traversed section tree + // but sections don't need to be sorted within regions + var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; + var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; + var cameraZ = sectionPos.getZ() >> RenderRegion.REGION_LENGTH_SH; + + var size = unsorted.size(); + var sortItems = this.ensureSortItemsOfLength(size); + + for (var i = 0; i < size; i++) { + var region = unsorted.get(i).getRegion(); + var x = Math.abs(region.getX() - cameraX); + var y = Math.abs(region.getY() - cameraY); + var z = Math.abs(region.getZ() - cameraZ); + sortItems[i] = (x + y + z) << 16 | i; + } + + IntArrays.unstableSort(sortItems, 0, size); + + var sorted = new ObjectArrayList(size); + for (var i = 0; i < size; i++) { + var key = sortItems[i]; + var renderList = unsorted.get(key & 0xFFFF); + sorted.add(renderList); + } + + for (var list : sorted) { + list.prepareForRender(sectionPos, this); + } + + return new SortedRenderLists(sorted); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderSectionVisitor.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderSectionVisitor.java new file mode 100644 index 0000000000..2be54f4e56 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderSectionVisitor.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public interface RenderSectionVisitor { + void visit(RenderSection section); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SectionCollector.java new file mode 100644 index 0000000000..5e540babbf --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SectionCollector.java @@ -0,0 +1,106 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateTypes; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; + +import java.util.ArrayDeque; +import java.util.EnumMap; +import java.util.Map; +import java.util.Queue; + +public abstract class SectionCollector implements RenderListProvider, RenderSectionVisitor { + private final int frame; + private final TaskQueueType importantRebuildQueueType; + private final TaskQueueType importantSortQueueType; + private final ObjectArrayList renderLists; + private final EnumMap> sortedTaskLists; + private boolean needsRevisitForPendingUpdates = false; + + private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; + + public SectionCollector(int frame, TaskQueueType importantRebuildQueueType, TaskQueueType importantSortQueueType) { + this.frame = frame; + this.importantRebuildQueueType = importantRebuildQueueType; + this.importantSortQueueType = importantSortQueueType; + + this.renderLists = new ObjectArrayList<>(); + this.sortedTaskLists = new EnumMap<>(TaskQueueType.class); + + for (var type : TaskQueueType.values()) { + this.sortedTaskLists.put(type, new ArrayDeque<>()); + } + } + + private void visit(RenderSection section, int flags) { + // only process section (and associated render list) if it has content that needs rendering + if (flags != 0) { + RenderRegion region = section.getRegion(); + ChunkRenderList renderList = region.getRenderList(); + + if (renderList.getLastVisibleFrame() != this.frame) { + renderList.reset(this.frame, this.orderIsSorted()); + + this.renderLists.add(renderList); + } + + renderList.add(section.getSectionIndex(), flags); + } + + // always add to rebuild lists though, because it might just not be built yet + var pendingUpdate = section.getPendingUpdate(); + + if (pendingUpdate != 0) { + // if the section has a pending update but a task is already running for it, + // don't add it to the task list again because starting a new task when there's already one running is invalid. + // (for example, it would become impossible to cancel the earlier task) + if (section.getRunningJob() != null) { + this.needsRevisitForPendingUpdates = true; + return; + } + + var queueType = ChunkUpdateTypes.getQueueType(pendingUpdate, this.importantRebuildQueueType, this.importantSortQueueType); + Queue queue = this.sortedTaskLists.get(queueType); + + if (queue.size() < queueType.queueSizeLimit()) { + queue.add(section); + } + } + } + + @Override + public void visit(RenderSection section) { + this.visit(section, section.getFlags()); + } + + public void visitWithFlags(RenderSection section, int flags) { + this.visit(section, flags); + } + + @Override + public ObjectArrayList getUnsortedRenderLists() { + return this.renderLists; + } + + @Override + public Map> getTaskLists() { + return this.sortedTaskLists; + } + + @Override + public boolean needsRevisitForPendingUpdates() { + return this.needsRevisitForPendingUpdates; + } + + @Override + public void setCachedSortItems(int[] sortItems) { + SectionCollector.sortItems = sortItems; + } + + @Override + public int[] getCachedSortItems() { + return SectionCollector.sortItems; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortItemsProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortItemsProvider.java new file mode 100644 index 0000000000..3ce542af1e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortItemsProvider.java @@ -0,0 +1,16 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +public interface SortItemsProvider { + int[] getCachedSortItems(); + + void setCachedSortItems(int[] sortItems); + + default int[] ensureSortItemsOfLength(int length) { + var sortItems = this.getCachedSortItems(); + if (sortItems == null || sortItems.length < length) { + sortItems = new int[length]; + this.setCachedSortItems(sortItems); + } + return sortItems; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortedRenderLists.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortedRenderLists.java index 1837e0ae9d..080530bf11 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortedRenderLists.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/SortedRenderLists.java @@ -1,9 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleObjectArrayIterator; -import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; /** * Stores one render list of sections per region, sorted by the order in which @@ -27,44 +25,4 @@ public ReversibleObjectArrayIterator iterator(boolean reverse) public static SortedRenderLists empty() { return EMPTY; } - - public static class Builder { - private final ObjectArrayList lists = new ObjectArrayList<>(); - private final int frame; - - public Builder(int frame) { - this.frame = frame; - } - - public void add(RenderSection section) { - RenderRegion region = section.getRegion(); - ChunkRenderList list = region.getRenderList(); - - // Even if a section does not have render objects, we must ensure the render list is initialized and put - // into the sorted queue of lists, so that we maintain the correct order of draw calls. - if (list.getLastVisibleFrame() != this.frame) { - list.reset(this.frame); - - this.lists.add(list); - } - - // Only add the section to the render list if it actually contains render objects - if (section.getFlags() != 0) { - list.add(section); - } - } - - public SortedRenderLists build() { - var filtered = new ObjectArrayList(this.lists.size()); - - // Filter any empty render lists - for (var list : this.lists) { - if (list.size() > 0) { - filtered.add(list); - } - } - - return new SortedRenderLists(filtered); - } - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TreeSectionCollector.java new file mode 100644 index 0000000000..33edb33720 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TreeSectionCollector.java @@ -0,0 +1,32 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType; +import net.minecraft.core.SectionPos; + +/** + * Collects sections from a tree traversal. It needs to turn coordinates into section objects because the section collector is not capable of handling raw section indexes yet. + */ +public class TreeSectionCollector extends SectionCollector implements CoordinateSectionVisitor { + private final Long2ReferenceMap sections; + + public TreeSectionCollector(int frame, TaskQueueType importantRebuildQueueType, TaskQueueType importantSortQueueType, Long2ReferenceMap sections) { + super(frame, importantRebuildQueueType, importantSortQueueType); + this.sections = sections; + } + + @Override + public void visit(int x, int y, int z) { + var section = this.sections.get(SectionPos.asLong(x, y, z)); + + if (section != null) { + this.visit(section); + } + } + + @Override + public boolean orderIsSorted() { + return true; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java index 5ed657795f..e69de29bb2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java @@ -1,71 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.lists; - -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; -import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; -import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; - -import java.util.*; - -/** - * The visible chunk collector is passed to the occlusion graph search culler to - * collect the visible chunks. - */ -public class VisibleChunkCollector implements OcclusionCuller.Visitor { - private final ObjectArrayList sortedRenderLists; - private final EnumMap> sortedRebuildLists; - - private final int frame; - - public VisibleChunkCollector(int frame) { - this.frame = frame; - - this.sortedRenderLists = new ObjectArrayList<>(); - this.sortedRebuildLists = new EnumMap<>(ChunkUpdateType.class); - - for (var type : ChunkUpdateType.values()) { - this.sortedRebuildLists.put(type, new ArrayDeque<>()); - } - } - - @Override - public void visit(RenderSection section, boolean visible) { - RenderRegion region = section.getRegion(); - ChunkRenderList renderList = region.getRenderList(); - - // Even if a section does not have render objects, we must ensure the render list is initialized and put - // into the sorted queue of lists, so that we maintain the correct order of draw calls. - if (renderList.getLastVisibleFrame() != this.frame) { - renderList.reset(this.frame); - - this.sortedRenderLists.add(renderList); - } - - if (visible && section.getFlags() != 0) { - renderList.add(section); - } - - this.addToRebuildLists(section); - } - - private void addToRebuildLists(RenderSection section) { - ChunkUpdateType type = section.getPendingUpdate(); - - if (type != null && section.getTaskCancellationToken() == null) { - Queue queue = this.sortedRebuildLists.get(type); - - if (queue.size() < type.getMaximumQueueSize()) { - queue.add(section); - } - } - } - - public SortedRenderLists createRenderLists() { - return new SortedRenderLists(this.sortedRenderLists); - } - - public Map> getRebuildLists() { - return this.sortedRebuildLists; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 95288386bb..5f0d9b3074 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -2,6 +2,7 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.RenderSectionVisitor; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.caffeinemc.mods.sodium.client.util.collections.DoubleBufferedQueue; @@ -18,12 +19,16 @@ public class OcclusionCuller { private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); + private int outOfWorldRadius; + private int outOfWorldHeight; + private int outOfWorldDirection; + public OcclusionCuller(Long2ReferenceMap sections, Level level) { this.sections = sections; this.level = level; } - public void findVisible(Visitor visitor, + public void findVisible(RenderSectionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, @@ -32,14 +37,29 @@ public void findVisible(Visitor visitor, final var queues = this.queue; queues.reset(); - this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); + var initWriteQueue = this.queue.write(); + this.init(visitor, initWriteQueue, viewport, useOcclusionCulling, frame); + + // initial write so that the first flip doesn't stop the loop + if (this.outOfWorldRadius == 0) { + while (initWriteQueue.isEmpty() && this.initOutsideWorldHeight(initWriteQueue, viewport, searchDistance, frame)) { + this.outOfWorldRadius++; + } + } while (queues.flip()) { + if (this.outOfWorldRadius > 0) { + this.initOutsideWorldHeight(queues.write(), viewport, searchDistance, frame); + this.outOfWorldRadius++; + } + processQueue(visitor, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); } + + this.addNearbySections(visitor, viewport, frame); } - private static void processQueue(Visitor visitor, + private static void processQueue(RenderSectionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, @@ -50,13 +70,12 @@ private static void processQueue(Visitor visitor, RenderSection section; while ((section = readQueue.dequeue()) != null) { - boolean visible = isSectionVisible(section, viewport, searchDistance); - visitor.visit(section, visible); - - if (!visible) { + if (!isSectionVisible(section, viewport, searchDistance)) { continue; } + visitor.visit(section); + int connections; { @@ -188,9 +207,10 @@ private static boolean isWithinRenderDistance(CameraTransform camera, RenderSect // coordinates of the point to compare (in view space) // this is the closest point within the bounding box to the center (0, 0, 0) - float dx = nearestToZero(ox, ox + 16) - camera.fracX; - float dy = nearestToZero(oy, oy + 16) - camera.fracY; - float dz = nearestToZero(oz, oz + 16) - camera.fracZ; + // the bounding box is expanded by 1 block in each direction due to the maximum allowed size of block models. + float dx = nearestToZero(ox - 1, ox + 17) - camera.fracX; + float dy = nearestToZero(oy - 1, oy + 17) - camera.fracY; + float dz = nearestToZero(oz - 1, oz + 17) - camera.fracZ; // vanilla's "cylindrical fog" algorithm // max(length(distance.xz), abs(distance.y)) @@ -206,20 +226,50 @@ private static int nearestToZero(int min, int max) { return clamped; } - // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models - // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon - // to deal with floating point imprecision during a frustum check (see GH#2132). - private static final float CHUNK_SECTION_SIZE = 8.0f /* chunk bounds */ + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; - public static boolean isWithinFrustum(Viewport viewport, RenderSection section) { - return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), - CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE); + return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ()); + } + + // Only used for nearby sections with large models + public static boolean isWithinNearbySectionFrustum(Viewport viewport, RenderSection section) { + return viewport.isBoxVisibleLooser(section.getCenterX(), section.getCenterY(), section.getCenterZ()); } - private void init(Visitor visitor, + // This method visits sections near the origin that are not in the path of the graph traversal + // but have bounding boxes that may intersect with the frustum. It does this additional check + // for all neighboring, even diagonally neighboring, sections around the origin to render them + // if their extended bounding box is visible, and they may render large models that extend + // outside the 16x16x16 base volume of the section. + private void addNearbySections(RenderSectionVisitor visitor, Viewport viewport, int frame) { + var origin = viewport.getChunkCoord(); + var originX = origin.getX(); + var originY = origin.getY(); + var originZ = origin.getZ(); + + for (var dx = -1; dx <= 1; dx++) { + for (var dy = -1; dy <= 1; dy++) { + for (var dz = -1; dz <= 1; dz++) { + if (dx == 0 && dy == 0 && dz == 0) { + continue; + } + + var section = this.getRenderSection(originX + dx, originY + dy, originZ + dz); + + // additionally render not yet visited but visible sections + if (section != null && section.getLastVisibleFrame() != frame && isWithinNearbySectionFrustum(viewport, section)) { + // reset state on first visit, but don't enqueue + section.setLastVisibleFrame(frame); + + visitor.visit(section); + } + } + } + } + } + + private void init(RenderSectionVisitor visitor, WriteQueue queue, Viewport viewport, - float searchDistance, boolean useOcclusionCulling, int frame) { @@ -227,18 +277,21 @@ private void init(Visitor visitor, if (origin.getY() < this.level.getMinSection()) { // below the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, - this.level.getMinSection(), GraphDirection.DOWN); + this.outOfWorldRadius = 0; + this.outOfWorldHeight = this.level.getMinSection(); + this.outOfWorldDirection = GraphDirection.DOWN; } else if (origin.getY() >= this.level.getMaxSection()) { // above the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, - this.level.getMaxSection() - 1, GraphDirection.UP); + this.outOfWorldRadius = 0; + this.outOfWorldHeight = this.level.getMaxSection() - 1; + this.outOfWorldDirection = GraphDirection.UP; } else { + this.outOfWorldRadius = -1; this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, frame); } } - private void initWithinWorld(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { + private void initWithinWorld(RenderSectionVisitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { var origin = viewport.getChunkCoord(); var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); @@ -249,7 +302,7 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V section.setLastVisibleFrame(frame); section.setIncomingDirections(GraphDirectionSet.NONE); - visitor.visit(section, true); + visitor.visit(section); int outgoing; @@ -268,21 +321,23 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V // Enqueues sections that are inside the viewport using diamond spiral iteration to avoid sorting and ensure a // consistent order. Innermost layers are enqueued first. Within each layer, iteration starts at the northernmost // section and proceeds counterclockwise (N->W->S->E). - private void initOutsideWorldHeight(WriteQueue queue, + private boolean initOutsideWorldHeight(WriteQueue queue, Viewport viewport, float searchDistance, - int frame, - int height, - int direction) - { + int frame) { var origin = viewport.getChunkCoord(); var radius = Mth.floor(searchDistance / 16.0f); + var height = this.outOfWorldHeight; + var direction = this.outOfWorldDirection; + var layer = this.outOfWorldRadius; // Layer 0 - this.tryVisitNode(queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); + if (layer == 0) { + this.tryVisitNode(queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); + } // Complete layers, excluding layer 0 - for (int layer = 1; layer <= radius; layer++) { + else if (layer <= radius) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); @@ -295,7 +350,7 @@ private void initOutsideWorldHeight(WriteQueue queue, } // Incomplete layers - for (int layer = radius + 1; layer <= 2 * radius; layer++) { + else if (layer <= 2 * radius) { int l = layer - radius; for (int z = -radius; z <= -l; z++) { @@ -318,6 +373,12 @@ private void initOutsideWorldHeight(WriteQueue queue, this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } } + + // nothing more to init + else { + return false; + } + return true; } private void tryVisitNode(WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { @@ -334,7 +395,4 @@ private RenderSection getRenderSection(int x, int y, int z) { return this.sections.get(SectionPos.asLong(x, y, z)); } - public interface Visitor { - void visit(RenderSection section, boolean visible); - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index df6df676d4..0d1f2be7f4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -5,7 +5,9 @@ import net.caffeinemc.mods.sodium.client.gl.arena.staging.StagingBuffer; import net.caffeinemc.mods.sodium.client.gl.buffer.GlBuffer; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; +import net.caffeinemc.mods.sodium.client.gl.device.MultiDrawBatch; import net.caffeinemc.mods.sodium.client.gl.tessellation.GlTessellation; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.data.SectionRenderDataStorage; import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; @@ -20,17 +22,21 @@ import java.util.Map; public class RenderRegion { + public static final int SECTION_VERTEX_COUNT_ESTIMATE = 756; + public static final int SECTION_INDEX_COUNT_ESTIMATE = (SECTION_VERTEX_COUNT_ESTIMATE / DefaultTerrainRenderPasses.ALL.length / 4) * 6; + public static final int SECTION_BUFFER_ESTIMATE = SECTION_VERTEX_COUNT_ESTIMATE * ChunkMeshFormats.COMPACT.getVertexFormat().getStride() + SECTION_INDEX_COUNT_ESTIMATE * Integer.BYTES; + public static final int REGION_WIDTH = 8; public static final int REGION_HEIGHT = 4; public static final int REGION_LENGTH = 8; - private static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1; - private static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1; - private static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1; + public static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1; + public static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1; + public static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1; - protected static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M); - protected static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M); - protected static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M); + public static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M); + public static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M); + public static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M); public static final int REGION_SIZE = REGION_WIDTH * REGION_HEIGHT * REGION_LENGTH; @@ -51,6 +57,8 @@ public class RenderRegion { private final Map sectionRenderData = new Reference2ReferenceOpenHashMap<>(); private DeviceResources resources; + private final Map cachedBatches = new Reference2ReferenceOpenHashMap<>(); + public RenderRegion(int x, int y, int z, StagingBuffer stagingBuffer) { this.x = x; this.y = y; @@ -64,6 +72,18 @@ public static long key(int x, int y, int z) { return SectionPos.asLong(x, y, z); } + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + public int getChunkX() { return this.x << REGION_WIDTH_SH; } @@ -101,6 +121,35 @@ public void delete(CommandList commandList) { } Arrays.fill(this.sections, null); + + for (var batch : this.cachedBatches.values()) { + batch.delete(); + } + this.cachedBatches.clear(); + } + + public void clearAllCachedBatches() { + for (var batch : this.cachedBatches.values()) { + batch.clear(); + } + } + + public void clearCachedBatchFor(TerrainRenderPass pass) { + var batch = this.cachedBatches.get(pass); + if (batch != null) { + batch.clear(); + } + } + + public MultiDrawBatch getCachedBatch(TerrainRenderPass pass) { + MultiDrawBatch batch = this.cachedBatches.get(pass); + if (batch != null) { + return batch; + } + + batch = new MultiDrawBatch((ModelQuadFacing.COUNT * RenderRegion.REGION_SIZE) + 1); + this.cachedBatches.put(pass, batch); + return batch; } public boolean isEmpty() { @@ -170,6 +219,10 @@ public void removeSection(RenderSection section) { this.sections[sectionIndex] = null; this.sectionCount--; } + + public float getFillFractionInv() { + return (float) RenderRegion.REGION_SIZE / (float) this.sectionCount; + } public RenderSection getSection(int id) { return this.sections[id]; @@ -191,6 +244,7 @@ public void update(CommandList commandList) { if (this.resources != null && this.resources.shouldDelete()) { this.resources.delete(commandList); this.resources = null; + this.clearAllCachedBatches(); } } @@ -215,11 +269,8 @@ public static class DeviceResources { public DeviceResources(CommandList commandList, StagingBuffer stagingBuffer) { int stride = ChunkMeshFormats.COMPACT.getVertexFormat().getStride(); - // the magic number 756 for the initial size is arbitrary, it was made up. - var initialVertices = 756; - this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * initialVertices, stride, stagingBuffer); - var initialIndices = (initialVertices / 4) * 6; - this.indexArena = new GlBufferArena(commandList, REGION_SIZE * initialIndices, Integer.BYTES, stagingBuffer); + this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_VERTEX_COUNT_ESTIMATE, stride, stagingBuffer); + this.indexArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_INDEX_COUNT_ESTIMATE, Integer.BYTES, stagingBuffer); } public void updateTessellation(CommandList commandList, GlTessellation tessellation) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 71665e93a2..0b8213cddf 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -17,7 +17,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; - +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.SharedIndexSorter; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -74,74 +74,104 @@ private void uploadResults(CommandList commandList, RenderRegion region, Collect if (storage != null) { storage.removeVertexData(renderSectionIndex); + region.clearCachedBatchFor(pass); } BuiltSectionMeshParts mesh = chunkBuildOutput.getMesh(pass); if (mesh != null) { uploads.add(new PendingSectionMeshUpload(result.render, mesh, pass, - new PendingUpload(mesh.getVertexData()))); + new PendingUpload(mesh.getVertexData()))); } } } if (result instanceof ChunkSortOutput indexDataOutput && !indexDataOutput.isReusingUploadedIndexData()) { - var buffer = indexDataOutput.getIndexBuffer(); + var sorter = indexDataOutput.getSorter(); + if (sorter instanceof SharedIndexSorter sharedIndexSorter) { + var storage = region.createStorage(DefaultTerrainRenderPasses.TRANSLUCENT); + storage.removeIndexData(renderSectionIndex); - // when a non-present TranslucentData is used like NoData, the indexBuffer is null - if (buffer == null) { - continue; - } + // clear batch cache if it's newly using the shared index buffer and was not previously. + // updates to the shared index buffer which cause the batch cache to be invalidated are handled with needsSharedIndexUpdate + if (storage.setSharedIndexUsage(renderSectionIndex, sharedIndexSorter.quadCount())) { + region.clearCachedBatchFor(DefaultTerrainRenderPasses.TRANSLUCENT); + } + } else { + var storage = region.getStorage(DefaultTerrainRenderPasses.TRANSLUCENT); + if (storage != null) { + storage.removeIndexData(renderSectionIndex); + storage.setSharedIndexUsage(renderSectionIndex, 0); - indexUploads.add(new PendingSectionIndexBufferUpload(result.render, new PendingUpload(buffer))); + // always clear batch cache on uploads of new index data + region.clearCachedBatchFor(DefaultTerrainRenderPasses.TRANSLUCENT); + } - var storage = region.getStorage(DefaultTerrainRenderPasses.TRANSLUCENT); - if (storage != null) { - storage.removeIndexData(renderSectionIndex); + if (sorter == null) { + continue; + } + // when a non-present TranslucentData is used like NoData, the indexBuffer is null + var buffer = sorter.getIndexBuffer(); + if (buffer == null) { + continue; + } + + indexUploads.add(new PendingSectionIndexBufferUpload(result.render, new PendingUpload(buffer))); } } } // If we have nothing to upload, abort! - if (uploads.isEmpty() && indexUploads.isEmpty()) { + var translucentStorage = region.getStorage(DefaultTerrainRenderPasses.TRANSLUCENT); + var needsSharedIndexUpdate = translucentStorage != null && translucentStorage.needsSharedIndexUpdate(); + if (uploads.isEmpty() && indexUploads.isEmpty() && !needsSharedIndexUpdate) { return; } var resources = region.createResources(commandList); + var regionFillFractionInv = region.getFillFractionInv(); if (!uploads.isEmpty()) { var arena = resources.getGeometryArena(); boolean bufferChanged = arena.upload(commandList, uploads.stream() - .map(upload -> upload.vertexUpload)); + .map(upload -> upload.vertexUpload), regionFillFractionInv); // If any of the buffers changed, the tessellation will need to be updated // Once invalidated the tessellation will be re-created on the next attempted use if (bufferChanged) { region.refreshTesselation(commandList); + region.clearAllCachedBatches(); } // Collect the upload results for (PendingSectionMeshUpload upload : uploads) { var storage = region.createStorage(upload.pass); storage.setVertexData(upload.section.getSectionIndex(), - upload.vertexUpload.getResult(), upload.meshData.getVertexCounts()); + upload.vertexUpload.getResult(), upload.meshData.getVertexSegments()); } } + var indexBufferChanged = false; + if (!indexUploads.isEmpty()) { var arena = resources.getIndexArena(); - boolean bufferChanged = arena.upload(commandList, indexUploads.stream() - .map(upload -> upload.indexBufferUpload)); - - if (bufferChanged) { - region.refreshIndexedTesselation(commandList); - } + indexBufferChanged = arena.upload(commandList, indexUploads.stream() + .map(upload -> upload.indexBufferUpload), regionFillFractionInv); for (PendingSectionIndexBufferUpload upload : indexUploads) { var storage = region.createStorage(DefaultTerrainRenderPasses.TRANSLUCENT); storage.setIndexData(upload.section.getSectionIndex(), upload.indexBufferUpload.getResult()); } } + + if (needsSharedIndexUpdate) { + indexBufferChanged |= translucentStorage.updateSharedIndexData(commandList, resources.getIndexArena(), regionFillFractionInv); + } + + if (indexBufferChanged) { + region.refreshIndexedTesselation(commandList); + region.clearCachedBatchFor(DefaultTerrainRenderPasses.TRANSLUCENT); + } } private Reference2ReferenceMap.FastEntrySet> createMeshUploadQueues(Collection results) { @@ -196,7 +226,6 @@ private record PendingSectionMeshUpload(RenderSection section, BuiltSectionMeshP private record PendingSectionIndexBufferUpload(RenderSection section, PendingUpload indexBufferUpload) { } - private static StagingBuffer createStagingBuffer(CommandList commandList) { if (SodiumClientMod.options().advanced.useAdvancedStagingBuffers && MappedStagingBuffer.isSupported(RenderDevice.INSTANCE)) { return new MappedStagingBuffer(commandList); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java index 6855970186..5068200bb0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/ChunkShaderFogComponent.java @@ -10,7 +10,7 @@ * 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. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/DefaultShaderInterface.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/DefaultShaderInterface.java index 1ce8947477..4417b93232 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/DefaultShaderInterface.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/shader/DefaultShaderInterface.java @@ -1,10 +1,16 @@ package net.caffeinemc.mods.sodium.client.render.chunk.shader; import com.mojang.blaze3d.platform.GlStateManager; +import net.caffeinemc.mods.sodium.client.gl.device.GLRenderDevice; +import net.caffeinemc.mods.sodium.client.gl.shader.uniform.GlUniformFloat2v; import net.caffeinemc.mods.sodium.client.gl.shader.uniform.GlUniformFloat3v; import net.caffeinemc.mods.sodium.client.gl.shader.uniform.GlUniformInt; import net.caffeinemc.mods.sodium.client.gl.shader.uniform.GlUniformMatrix4f; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.impl.CompactChunkVertex; import net.caffeinemc.mods.sodium.client.util.TextureUtil; +import net.caffeinemc.mods.sodium.mixin.core.render.texture.TextureAtlasAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlas; import org.joml.Matrix4fc; import org.lwjgl.opengl.GL32C; @@ -20,14 +26,16 @@ public class DefaultShaderInterface implements ChunkShaderInterface { private final GlUniformMatrix4f uniformModelViewMatrix; private final GlUniformMatrix4f uniformProjectionMatrix; private final GlUniformFloat3v uniformRegionOffset; + private final GlUniformFloat2v uniformTexCoordShrink; - // The fog shader component used by this program in order to setup the appropriate GL state + // The fog shader component used by this program in order to set up the appropriate GL state private final ChunkShaderFogComponent fogShader; public DefaultShaderInterface(ShaderBindingContext context, ChunkShaderOptions options) { this.uniformModelViewMatrix = context.bindUniform("u_ModelViewMatrix", GlUniformMatrix4f::new); this.uniformProjectionMatrix = context.bindUniform("u_ProjectionMatrix", GlUniformMatrix4f::new); this.uniformRegionOffset = context.bindUniform("u_RegionOffset", GlUniformFloat3v::new); + this.uniformTexCoordShrink = context.bindUniform("u_TexCoordShrink", GlUniformFloat2v::new); this.uniformTextures = new EnumMap<>(ChunkShaderTextureSlot.class); this.uniformTextures.put(ChunkShaderTextureSlot.BLOCK, context.bindUniform("u_BlockTex", GlUniformInt::new)); @@ -38,9 +46,25 @@ public DefaultShaderInterface(ShaderBindingContext context, ChunkShaderOptions o @Override // the shader interface should not modify pipeline state public void setupState() { + // TODO: Bind to these textures directly rather than using fragile RenderSystem state this.bindTexture(ChunkShaderTextureSlot.BLOCK, TextureUtil.getBlockTextureId()); this.bindTexture(ChunkShaderTextureSlot.LIGHT, TextureUtil.getLightTextureId()); + var textureAtlas = (TextureAtlasAccessor) Minecraft.getInstance() + .getTextureManager() + .getTexture(TextureAtlas.LOCATION_BLOCKS); + + // There is a limited amount of sub-texel precision when using hardware texture sampling. The mapped texture + // area must be "shrunk" by at least one sub-texel to avoid bleed between textures in the atlas. And since we + // offset texture coordinates in the vertex format by one texel, we also need to undo that here. + double subTexelPrecision = (1 << GLRenderDevice.INSTANCE.getSubTexelPrecisionBits()); + double subTexelOffset = 1.0f / CompactChunkVertex.TEXTURE_MAX_VALUE; + + this.uniformTexCoordShrink.set( + (float) (subTexelOffset - (((1.0D / textureAtlas.sodium$getWidth()) / subTexelPrecision))), + (float) (subTexelOffset - (((1.0D / textureAtlas.sodium$getHeight()) / subTexelPrecision))) + ); + this.fogShader.setup(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/AlignableNormal.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/AlignableNormal.java deleted file mode 100644 index ef492d0f1a..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/AlignableNormal.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; - -import org.joml.Vector3f; -import org.joml.Vector3fc; - -import it.unimi.dsi.fastutil.floats.FloatArrays; -import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; - -/** - * A normal vector that has additional information about its alignment. This is - * useful for better hashing and telling other code that the normal is aligned, - * which in turn enables many optimizations and fast paths to be taken. - */ -public class AlignableNormal extends Vector3f { - private static final AlignableNormal[] NORMALS = new AlignableNormal[ModelQuadFacing.DIRECTIONS]; - - static { - for (int i = 0; i < ModelQuadFacing.DIRECTIONS; i++) { - NORMALS[i] = new AlignableNormal(ModelQuadFacing.ALIGNED_NORMALS[i], i); - } - } - - private static final int UNASSIGNED = ModelQuadFacing.UNASSIGNED.ordinal(); - private final int alignedDirection; - - private AlignableNormal(Vector3fc v, int alignedDirection) { - super(v); - this.alignedDirection = alignedDirection; - } - - public static AlignableNormal fromAligned(int alignedDirection) { - return NORMALS[alignedDirection]; - } - - public static AlignableNormal fromUnaligned(Vector3fc v) { - return new AlignableNormal(v, UNASSIGNED); - } - - public int getAlignedDirection() { - return this.alignedDirection; - } - - public boolean isAligned() { - return this.alignedDirection != UNASSIGNED; - } - - @Override - public int hashCode() { - if (this.isAligned()) { - return this.alignedDirection; - } else { - return super.hashCode() + ModelQuadFacing.DIRECTIONS; - } - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - AlignableNormal other = (AlignableNormal) obj; - return this.alignedDirection == other.alignedDirection; - } - - public static boolean queryRange(float[] sortedDistances, float start, float end) { - // test that there is actually an entry in the query range - int result = FloatArrays.binarySearch(sortedDistances, start); - if (result < 0) { - // recover the insertion point - int insertionPoint = -result - 1; - if (insertionPoint >= sortedDistances.length) { - // no entry in the query range - return false; - } - - // check if the entry at the insertion point, which is the next one greater than - // the start value, is less than or equal to the end value - if (sortedDistances[insertionPoint] <= end) { - // there is an entry in the query range - return true; - } - } else { - // exact match, trigger - return true; - } - return false; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/QuadSplittingMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/QuadSplittingMode.java new file mode 100644 index 0000000000..3b896d82f7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/QuadSplittingMode.java @@ -0,0 +1,49 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; + +import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +public enum QuadSplittingMode implements TextProvider { + OFF("/", 1.0f, true, "sodium.options.quad_splitting.off"), + SAFE("S", 2.0f, true, "sodium.options.quad_splitting.safe"), + UNLIMITED("U", Float.POSITIVE_INFINITY, false, "sodium.options.quad_splitting.unlimited"); + + private final String shortName; + + // how much bigger the final geometry is allowed to be compared to the input geometry when performing quad splitting. + private final float maxAmplificationFactor; + private final boolean quantizeTriggerNormals; + private final Component name; + + QuadSplittingMode(String shortName, float maxAmplificationFactor, boolean quantizeTriggerNormals, String name) { + this.shortName = shortName; + this.maxAmplificationFactor = maxAmplificationFactor; + this.quantizeTriggerNormals = quantizeTriggerNormals; + this.name = Component.translatable(name); + } + + @Override + public Component getLocalizedName() { + return this.name; + } + + public String getShortName() { + return this.shortName; + } + + public boolean allowsSplitting() { + return this != OFF; + } + + public boolean quantizeTriggerNormals() { + return this.quantizeTriggerNormals; + } + + public int getMaxTotalQuads(int baseQuadCount) { + if (Float.isInfinite(this.maxAmplificationFactor)) { + return Integer.MAX_VALUE; + } + return Mth.ceil(baseQuadCount * this.maxAmplificationFactor); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java index 055270f7bf..c6b2cd5466 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java @@ -1,5 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; + public enum SortBehavior { OFF("OFF", SortMode.NONE), STATIC("S", SortMode.STATIC), @@ -12,10 +14,10 @@ public enum SortBehavior { private final String shortName; private final SortBehavior.SortMode sortMode; private final SortBehavior.PriorityMode priorityMode; - private final SortBehavior.DeferMode deferMode; + private final DeferMode deferMode; SortBehavior(String shortName, SortBehavior.SortMode sortMode, SortBehavior.PriorityMode priorityMode, - SortBehavior.DeferMode deferMode) { + DeferMode deferMode) { this.shortName = shortName; this.sortMode = sortMode; this.priorityMode = priorityMode; @@ -26,7 +28,7 @@ public enum SortBehavior { this(shortName, sortMode, null, null); } - SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, SortBehavior.DeferMode deferMode) { + SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, DeferMode deferMode) { this(shortName, SortMode.DYNAMIC, priorityMode, deferMode); } @@ -42,7 +44,7 @@ public SortBehavior.PriorityMode getPriorityMode() { return this.priorityMode; } - public SortBehavior.DeferMode getDeferMode() { + public DeferMode getDeferMode() { return this.deferMode; } @@ -53,8 +55,4 @@ public enum SortMode { public enum PriorityMode { NONE, NEARBY, ALL } - - public enum DeferMode { - ALWAYS, ONE_FRAME, ZERO_FRAMES - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortType.java index d00713259a..92db802af9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortType.java @@ -3,50 +3,54 @@ /** * What type of sorting to use for a section. Calculated by a heuristic after * building a section. + *

+ * Invariant: !(needsDirectionMixing && allowSliceReordering) */ public enum SortType { /** * The section is fully empty, no index buffer is needed. */ - EMPTY_SECTION(false), + EMPTY_SECTION(false, true), /** * The section has no translucent geometry, no index buffer is needed. */ - NO_TRANSLUCENT(false), + NO_TRANSLUCENT(false, true), /** * No sorting is required and the sort order doesn't matter. */ - NONE(false), + NONE(false, true), /** - * There is only one sort order. No active sorting is required, but an initial + * There is only one sort order. No active sorting is required, except for an initial * sort where quads of each facing are sorted according to their distances in * regard to their normal. - * - * Currently assumes that there are no UNASSIGNED quads present. If this - * changes, remove this note and adjust StaticTranslucentData and anything that - * reads from it to handle UNASSIGNED quads. */ - STATIC_NORMAL_RELATIVE(false), + STATIC_NORMAL_RELATIVE(false, false), /** * There is only one sort order and not active sorting is required, but * determining the static sort order involves doing a toplogical sort of the * quads. */ - STATIC_TOPO(true), + STATIC_TOPO(true, false), /** * There are multiple sort orders. Sorting is required every time GFNI triggers * this section. */ - DYNAMIC(true); + DYNAMIC(true, false); public final boolean needsDirectionMixing; + public final boolean allowSliceReordering; - SortType(boolean needsDirectionMixing) { + SortType(boolean needsDirectionMixing, boolean allowSliceReordering) { this.needsDirectionMixing = needsDirectionMixing; + this.allowSliceReordering = allowSliceReordering; + + if (needsDirectionMixing && allowSliceReordering) { + throw new IllegalArgumentException("Invalid sort type"); + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java index 2125b23b45..50b4a9b1f7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java @@ -5,19 +5,17 @@ import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.DefaultFluidRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.BSPBuildFailureException; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.*; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.FullTQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.RegularTQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.GeometryPlanes; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; -import org.joml.Vector3f; -import org.joml.Vector3fc; - -import java.util.Arrays; /** * The translucent geometry collector collects the data from the renderers and @@ -47,11 +45,13 @@ * task result. */ public class TranslucentGeometryCollector { - private final SectionPos sectionPos; + private final QuadSplittingMode quadSplittingMode = SodiumClientMod.options().performance.quadSplittingMode; + private final SortBehavior sortBehavior; // true if there are any unaligned quads private boolean hasUnaligned = false; + private int untrackedUnalignedNormalCount = 0; // a bitmap of the aligned facings present in the section private int alignedFacingBitmap = 0; @@ -83,6 +83,7 @@ public class TranslucentGeometryCollector { @SuppressWarnings("unchecked") private ReferenceArrayList[] quadLists = new ReferenceArrayList[ModelQuadFacing.COUNT]; + private final int[] meshFacingCounts = new int[ModelQuadFacing.COUNT]; private TQuad[] quads; private SortType sortType; @@ -90,148 +91,51 @@ public class TranslucentGeometryCollector { private boolean quadHashPresent = false; private int quadHash = 0; - public TranslucentGeometryCollector(SectionPos sectionPos) { + public TranslucentGeometryCollector(SectionPos sectionPos, SortBehavior sortBehavior) { this.sectionPos = sectionPos; + this.sortBehavior = sortBehavior; } - private static final float INV_QUANTIZE_EPSILON = 256f; - private static final float QUANTIZE_EPSILON = 1f / INV_QUANTIZE_EPSILON; - - static { - // ensure it fits with the fluid renderer epsilon and that it's a power-of-two - // fraction - var targetEpsilon = DefaultFluidRenderer.EPSILON * 2.1f; - if (QUANTIZE_EPSILON <= targetEpsilon && Integer.bitCount((int) INV_QUANTIZE_EPSILON) == 1) { - throw new RuntimeException("epsilon is invalid: " + QUANTIZE_EPSILON); - } - } - - public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, ModelQuadFacing facing) { - float xSum = 0; - float ySum = 0; - float zSum = 0; - - // keep track of distinct vertices to compute the center accurately for - // degenerate quads - float lastX = vertices[3].x; - float lastY = vertices[3].y; - float lastZ = vertices[3].z; - int uniqueVertexes = 0; - - float posXExtent = Float.NEGATIVE_INFINITY; - float posYExtent = Float.NEGATIVE_INFINITY; - float posZExtent = Float.NEGATIVE_INFINITY; - float negXExtent = Float.POSITIVE_INFINITY; - float negYExtent = Float.POSITIVE_INFINITY; - float negZExtent = Float.POSITIVE_INFINITY; - - for (int i = 0; i < 4; i++) { - float x = vertices[i].x; - float y = vertices[i].y; - float z = vertices[i].z; - - posXExtent = Math.max(posXExtent, x); - posYExtent = Math.max(posYExtent, y); - posZExtent = Math.max(posZExtent, z); - negXExtent = Math.min(negXExtent, x); - negYExtent = Math.min(negYExtent, y); - negZExtent = Math.min(negZExtent, z); - - if (x != lastX || y != lastY || z != lastZ) { - xSum += x; - ySum += y; - zSum += z; - uniqueVertexes++; - } - if (i != 3) { - lastX = x; - lastY = y; - lastZ = z; - } - } - - // shrink quad in non-normal directions to prevent intersections caused by - // epsilon offsets applied by FluidRenderer - if (facing != ModelQuadFacing.POS_X && facing != ModelQuadFacing.NEG_X) { - posXExtent -= QUANTIZE_EPSILON; - negXExtent += QUANTIZE_EPSILON; - if (negXExtent > posXExtent) { - negXExtent = posXExtent; - } - } - if (facing != ModelQuadFacing.POS_Y && facing != ModelQuadFacing.NEG_Y) { - posYExtent -= QUANTIZE_EPSILON; - negYExtent += QUANTIZE_EPSILON; - if (negYExtent > posYExtent) { - negYExtent = posYExtent; - } + /** + * Collects a quad to be sorted. Note that if the quad's geometry and/or its vertices make it axis-aligned but it still uses the {@link ModelQuadFacing#UNASSIGNED} facing, dynamic sorting will break. Checking every quad for this would slow things down. + * + * @param vertices the vertices of the quad + * @param facing the facing of the quad + * @param packedNormal the packed normal of the quad + * @return true if the quad is invalid and should be discarded from the model entirely, false otherwise. + */ + public boolean appendQuad(ChunkVertexEncoder.Vertex[] vertices, ModelQuadFacing facing, int packedNormal) { + TQuad quad; + if (this.isSplittingQuads()) { + quad = FullTQuad.fromVertices(vertices, facing, packedNormal); + } else { + quad = RegularTQuad.fromVertices(vertices, facing, packedNormal); } - if (facing != ModelQuadFacing.POS_Z && facing != ModelQuadFacing.NEG_Z) { - posZExtent -= QUANTIZE_EPSILON; - negZExtent += QUANTIZE_EPSILON; - if (negZExtent > posZExtent) { - negZExtent = posZExtent; - } + if (quad == null) { + return true; } - // POS_X, POS_Y, POS_Z, NEG_X, NEG_Y, NEG_Z - float[] extents = new float[] { posXExtent, posYExtent, posZExtent, negXExtent, negYExtent, negZExtent }; - int direction = facing.ordinal(); var quadList = this.quadLists[direction]; if (quadList == null) { quadList = new ReferenceArrayList<>(); this.quadLists[direction] = quadList; } - - Vector3fc center = null; - if (!facing.isAligned() || uniqueVertexes != 4) { - var centerX = xSum / uniqueVertexes; - var centerY = ySum / uniqueVertexes; - var centerZ = zSum / uniqueVertexes; - center = new Vector3f(centerX, centerY, centerZ); - } - - // check if we need to store vertex positions for this quad, only necessary if it's unaligned or rotated (yet aligned) - var needsVertexPositions = uniqueVertexes != 4 || !facing.isAligned(); - if (!needsVertexPositions) { - for (int i = 0; i < 4; i++) { - var vertex = vertices[i]; - if (vertex.x != posYExtent && vertex.x != negYExtent || - vertex.y != posZExtent && vertex.y != negZExtent || - vertex.z != posXExtent && vertex.z != negXExtent) { - needsVertexPositions = true; - break; - } - } - } - - float[] vertexPositions = null; - if (needsVertexPositions) { - vertexPositions = new float[12]; - for (int i = 0, itemIndex = 0; i < 4; i++) { - var vertex = vertices[i]; - vertexPositions[itemIndex++] = vertex.x; - vertexPositions[itemIndex++] = vertex.y; - vertexPositions[itemIndex++] = vertex.z; - } - } + quadList.add(quad); if (facing.isAligned()) { // only update global extents if there are no unaligned quads since this is only // used for the convex box test which doesn't work with unaligned quads anyway if (!this.hasUnaligned) { - this.extents[0] = Math.max(this.extents[0], posXExtent); - this.extents[1] = Math.max(this.extents[1], posYExtent); - this.extents[2] = Math.max(this.extents[2], posZExtent); - this.extents[3] = Math.min(this.extents[3], negXExtent); - this.extents[4] = Math.min(this.extents[4], negYExtent); - this.extents[5] = Math.min(this.extents[5], negZExtent); + var quadExtents = quad.getExtents(); + for (int i = 0; i < 3; i++) { + this.extents[i] = Math.max(this.extents[i], quadExtents[i]); + } + for (int i = 3; i < 6; i++) { + this.extents[i] = Math.min(this.extents[i], quadExtents[i]); + } } - var quad = TQuad.fromAligned(facing, extents, vertexPositions, center); - quadList.add(quad); - var extreme = this.alignedExtremes[direction]; var distance = quad.getAccurateDotProduct(); @@ -250,9 +154,6 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M } else { this.hasUnaligned = true; - var quad = TQuad.fromUnaligned(facing, extents, vertexPositions, center, packedNormal); - quadList.add(quad); - // update the two unaligned normals that are tracked var distance = quad.getAccurateDotProduct(); if (packedNormal == this.unalignedANormal) { @@ -273,8 +174,16 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M } else if (this.unalignedBNormal == -1) { this.unalignedBNormal = packedNormal; this.unalignedBDistance1 = distance; + } else { + this.untrackedUnalignedNormalCount++; } } + + return false; + } + + public boolean isSplittingQuads() { + return this.quadSplittingMode.allowsSplitting(); } /** @@ -283,8 +192,7 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M * * @param sortType the sort type to filter */ - private static SortType filterSortType(SortType sortType) { - SortBehavior sortBehavior = SodiumClientMod.options().performance.getSortBehavior(); + private static SortType filterSortType(SortType sortType, SortBehavior sortBehavior) { switch (sortBehavior) { case OFF: return SortType.NONE; @@ -348,15 +256,14 @@ private SortType sortTypeHeuristic() { return SortType.NONE; } - SortBehavior sortBehavior = SodiumClientMod.options().performance.getSortBehavior(); - if (sortBehavior.getSortMode() == SortBehavior.SortMode.NONE) { + if (this.sortBehavior.getSortMode() == SortBehavior.SortMode.NONE) { return SortType.NONE; } int alignedNormalCount = Integer.bitCount(this.alignedFacingBitmap); - int planeCount = getPlaneCount(alignedNormalCount); + int planeCount = this.getPlaneCount(alignedNormalCount); - int unalignedNormalCount = 0; + int unalignedNormalCount = this.untrackedUnalignedNormalCount; if (this.unalignedANormal != -1) { unalignedNormalCount++; } @@ -427,7 +334,6 @@ private SortType sortTypeHeuristic() { // use the given set of quad count limits to determine if a static topo sort // should be attempted - var attemptLimitIndex = Mth.clamp(normalCount, 2, STATIC_TOPO_SORT_ATTEMPT_LIMITS.length - 1); if (this.quads.length <= STATIC_TOPO_SORT_ATTEMPT_LIMITS[attemptLimitIndex]) { return SortType.STATIC_TOPO; @@ -472,6 +378,8 @@ public SortType finishRendering() { for (int direction = 0; direction < ModelQuadFacing.COUNT; direction++) { var quadList = this.quadLists[direction]; if (quadList != null) { + this.meshFacingCounts[direction] = quadList.size(); + for (var quad : quadList) { this.quads[quadIndex++] = quad; } @@ -482,36 +390,25 @@ public SortType finishRendering() { } this.quadLists = null; // they're not needed anymore - this.sortType = filterSortType(sortTypeHeuristic()); + this.sortType = filterSortType(this.sortTypeHeuristic(), this.sortBehavior); return this.sortType; } - private static int ensureUnassignedVertexCount(int[] vertexCounts) { - int vertexCount = vertexCounts[ModelQuadFacing.UNASSIGNED.ordinal()]; - - if (vertexCount == 0) { - throw new IllegalStateException("No unassigned data in mesh"); - } - - return vertexCount; - } - - private TranslucentData makeNewTranslucentData(int[] vertexCounts, CombinedCameraPos cameraPos, + private TranslucentData makeNewTranslucentData(CombinedCameraPos cameraPos, TranslucentData oldData) { if (this.sortType == SortType.NONE) { - return AnyOrderData.fromMesh(vertexCounts, this.quads, this.sectionPos); + return AnyOrderData.fromMesh(this.quads, this.sectionPos); } if (this.sortType == SortType.STATIC_NORMAL_RELATIVE) { var isDoubleUnaligned = this.alignedFacingBitmap == 0; - return StaticNormalRelativeData.fromMesh(vertexCounts, this.quads, this.sectionPos, isDoubleUnaligned); + return StaticNormalRelativeData.fromMesh(this.meshFacingCounts, this.quads, this.sectionPos, isDoubleUnaligned); } // from this point on we know the estimated sort type requires direction mixing // (no backface culling) and all vertices are in the UNASSIGNED direction. if (this.sortType == SortType.STATIC_TOPO) { - var vertexCount = ensureUnassignedVertexCount(vertexCounts); - var result = StaticTopoData.fromMesh(vertexCount, this.quads, this.sectionPos); + var result = StaticTopoData.fromMesh(this.quads, this.sectionPos, this.isSplittingQuads()); if (result != null) { return result; } @@ -519,21 +416,19 @@ private TranslucentData makeNewTranslucentData(int[] vertexCounts, CombinedCamer } // filter the sort type with the user setting and re-evaluate - this.sortType = filterSortType(this.sortType); + this.sortType = filterSortType(this.sortType, this.sortBehavior); if (this.sortType == SortType.NONE) { - return AnyOrderData.fromMesh(vertexCounts, this.quads, this.sectionPos); + return AnyOrderData.fromMesh(this.quads, this.sectionPos); } if (this.sortType == SortType.DYNAMIC) { - var vertexCount = ensureUnassignedVertexCount(vertexCounts); try { - return DynamicBSPData.fromMesh( - vertexCount, cameraPos, this.quads, this.sectionPos, oldData); + return DynamicBSPData.fromMesh(cameraPos, this.quads, this.sectionPos, oldData, this.quadSplittingMode); } catch (BSPBuildFailureException e) { var geometryPlanes = GeometryPlanes.fromQuadLists(this.sectionPos, this.quads); return DynamicTopoData.fromMesh( - vertexCount, cameraPos, this.quads, this.sectionPos, + cameraPos, this.quads, this.sectionPos, geometryPlanes); } } @@ -541,11 +436,12 @@ private TranslucentData makeNewTranslucentData(int[] vertexCounts, CombinedCamer throw new IllegalStateException("Unknown sort type: " + this.sortType); } - private int getQuadHash(TQuad[] quads) { + public int getQuadHash() { if (this.quadHashPresent) { return this.quadHash; } + var quads = this.quads; for (int i = 0; i < quads.length; i++) { var quad = quads[i]; this.quadHash = this.quadHash * 31 + quad.getQuadHash() + i * 3; @@ -555,41 +451,24 @@ private int getQuadHash(TQuad[] quads) { } public TranslucentData getTranslucentData( - TranslucentData oldData, BuiltSectionMeshParts translucentMesh, CombinedCameraPos cameraPos) { - // means there is no translucent geometry - if (translucentMesh == null) { + TranslucentData oldData, CombinedCameraPos cameraPos) { + if (this.quads.length == 0) { return NoData.forNoTranslucent(this.sectionPos); } - var vertexCounts = translucentMesh.getVertexCounts(); - // re-use the original translucent data if it's the same. This reduces the // amount of generated and uploaded index data when sections are rebuilt without // relevant changes to translucent geometry. Rebuilds happen when any part of // the section changes, including the here irrelevant cases of changes to opaque - // geometry or light levels. - if (oldData != null) { - // for the NONE sort type the ranges need to be the same, the actual geometry - // doesn't matter - if (this.sortType == SortType.NONE && oldData instanceof AnyOrderData oldAnyData - && oldAnyData.getQuadCount() == this.quads.length - && Arrays.equals(oldAnyData.getVertexCounts(), vertexCounts)) { - return oldAnyData; - } - - // for the other sort types the geometry needs to be the same (checked with - // length and hash) - if (oldData instanceof PresentTranslucentData oldPresentData) { - if (oldPresentData.getQuadCount() == this.quads.length - && oldPresentData.getQuadHash() == getQuadHash(this.quads)) { - return oldPresentData; - } - } + // geometry or non-geometric changes to translucent geometry. + // (except when quad splitting, where data is never reused) + if (oldData != null && oldData.oldDataMatches(this, this.sortType, this.quads)) { + return oldData; } - var newData = makeNewTranslucentData(vertexCounts, cameraPos, oldData); + var newData = this.makeNewTranslucentData(cameraPos, oldData); if (newData instanceof PresentTranslucentData presentData) { - presentData.setQuadHash(getQuadHash(this.quads)); + presentData.setQuadHash(this.getQuadHash()); } return newData; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java index 6362cbc07f..a199c17a14 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java @@ -1,19 +1,19 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; -import org.joml.Vector3fc; - import it.unimi.dsi.fastutil.ints.IntArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.api.util.NormI8; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.QuadSplittingMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TopoGraphSorting; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; -import net.caffeinemc.mods.sodium.api.util.NormI8; import net.minecraft.core.SectionPos; +import org.joml.Vector3fc; /** * A node in the BSP tree. The BSP tree is made up of nodes that split quads * into groups on either side of a plane and those that lie on the plane. * There's also leaf nodes that contain one or more quads. - * + *

* Implementation note: * - Doing a convex box test doesn't seem to bring a performance boost, even if * it does trigger sometimes with man-made structures. The multi partition node @@ -32,13 +32,13 @@ public void collectSortedQuads(NativeBuffer nativeBuffer, Vector3fc cameraPos) { } public static BSPResult buildBSP(TQuad[] quads, SectionPos sectionPos, BSPNode oldRoot, - boolean prepareNodeReuse) { + boolean prepareNodeReuse, boolean allowNodeReuse, QuadSplittingMode quadSplittingMode) { // throw if there's too many quads InnerPartitionBSPNode.validateQuadCount(quads.length); // create a workspace and then the nodes figure out the recursive building. // throws if the BSP can't be built, null if none is necessary - var workspace = new BSPWorkspace(quads, sectionPos, prepareNodeReuse); + var workspace = new BSPWorkspace(quads, sectionPos, prepareNodeReuse, allowNodeReuse, quadSplittingMode); // initialize the indexes to all quads int[] initialIndexes = new int[quads.length]; @@ -50,10 +50,11 @@ public static BSPResult buildBSP(TQuad[] quads, SectionPos sectionPos, BSPNode o var rootNode = BSPNode.build(workspace, allIndexes, -1, oldRoot); var result = workspace.result; result.setRootNode(rootNode); + result.setUpdatedQuadIndexes(workspace.getFinalizedUpdatedQuads()); return result; } - private static boolean doubleLeafPossible(TQuad quadA, TQuad quadB) { + private static boolean doubleLeafPossible(TQuad quadA, TQuad quadB, boolean failOnIntersection) { // check for coplanar or mutually invisible quads var facingA = quadA.getFacing(); var facingB = quadB.getFacing(); @@ -82,8 +83,8 @@ else if (facingA == facingB.getOpposite()) { // aligned otherwise mutually invisible else { - return !TopoGraphSorting.orthogonalQuadVisibleThrough(quadA, quadB) - && !TopoGraphSorting.orthogonalQuadVisibleThrough(quadB, quadA); + return !TopoGraphSorting.orthogonalQuadVisibleThrough(quadA, quadB, failOnIntersection) + && !TopoGraphSorting.orthogonalQuadVisibleThrough(quadB, quadA, failOnIntersection); } return false; @@ -100,10 +101,10 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS } else if (indexes.size() == 2) { var quadIndexA = indexes.getInt(0); var quadIndexB = indexes.getInt(1); - var quadA = workspace.quads[quadIndexA]; - var quadB = workspace.quads[quadIndexB]; + var quadA = workspace.get(quadIndexA); + var quadB = workspace.get(quadIndexB); - if (doubleLeafPossible(quadA, quadB)) { + if (doubleLeafPossible(quadA, quadB, workspace.canSplitQuads())) { return new LeafDoubleBSPNode(quadIndexA, quadIndexB); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPResult.java index 44911007b9..ec83ef2ae5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPResult.java @@ -8,6 +8,7 @@ */ public class BSPResult extends GeometryPlanes { private BSPNode rootNode; + private UpdatedQuadsList updatedQuadsList; public BSPNode getRootNode() { return this.rootNode; @@ -16,4 +17,12 @@ public BSPNode getRootNode() { public void setRootNode(BSPNode rootNode) { this.rootNode = rootNode; } + + public UpdatedQuadsList getUpdatedQuadsList() { + return this.updatedQuadsList; + } + + public void setUpdatedQuadIndexes(UpdatedQuadsList updatedQuadsList) { + this.updatedQuadsList = updatedQuadsList; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPSortState.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPSortState.java index 99b7ad87b4..eb9a87115d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPSortState.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPSortState.java @@ -1,13 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; -import java.nio.IntBuffer; -import java.lang.Math; - import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntConsumer; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; +import java.nio.IntBuffer; + /** * The sort state is passed around the tree (similar to visitor pattern) and * contains the index buffer being written to alongside additional state for @@ -55,10 +54,10 @@ private void checkModificationCounter(int reduceBy) { void writeIndex(int index) { if (this.indexMap != null) { TranslucentData.writeQuadVertexIndexes(this.indexBuffer, this.indexMap[index]); - checkModificationCounter(1); + this.checkModificationCounter(1); } else if (this.fixedIndexOffset != NO_FIXED_OFFSET) { TranslucentData.writeQuadVertexIndexes(this.indexBuffer, this.fixedIndexOffset + index); - checkModificationCounter(1); + this.checkModificationCounter(1); } else { TranslucentData.writeQuadVertexIndexes(this.indexBuffer, index); } @@ -103,12 +102,12 @@ static int[] compressIndexes(IntArrayList indexes) { /** * Compress a list of quad indexes by applying run length encoding or bit * packing to their deltas. - * + *

* Format: 32 bits, elements described as [length in bits: description] * header at position 0: 0b1[4: width index][10: delta count][17: first index] * header at position 1: 0b[32: base delta] * deltas at position 2..n: 0b[width: delta]... - * + *

* delta bit widths: * 1x32b, 2x16b, 3x10b, 4x8b, 5x6b, * 6x5b, 8x4b, 10x3b, 16x2b, 32x1b @@ -210,14 +209,13 @@ static int[] compressIndexes(IntArrayList indexes, boolean doSort) { return compressed; } - static int decompressOrRead(int[] indexes, IntConsumer consumer) { + static void decompressOrRead(int[] indexes, IntConsumer consumer) { if (isCompressed(indexes)) { - return decompress(indexes, consumer); + decompress(indexes, consumer); } else { for (int i = 0; i < indexes.length; i++) { consumer.accept(indexes[i]); } - return indexes.length; } } @@ -278,10 +276,10 @@ static boolean isCompressed(int[] indexes) { return indexes[0] < 0; } - private IntConsumer indexConsumer = (int index) -> TranslucentData.writeQuadVertexIndexes( + private final IntConsumer indexConsumer = (int index) -> TranslucentData.writeQuadVertexIndexes( this.indexBuffer, index); - private IntConsumer indexMapConsumer = (int index) -> TranslucentData.writeQuadVertexIndexes( + private final IntConsumer indexMapConsumer = (int index) -> TranslucentData.writeQuadVertexIndexes( this.indexBuffer, this.indexMap[index]); void writeIndexes(int[] indexes) { @@ -314,7 +312,7 @@ void writeIndexes(int[] indexes) { // check if the index modification session is over. this is very important or // there's an exception if (useIndexMap || useFixedIndexOffset) { - checkModificationCounter(valueCount); + this.checkModificationCounter(valueCount); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPWorkspace.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPWorkspace.java index 6860006cf4..fae176ab61 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPWorkspace.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPWorkspace.java @@ -1,40 +1,138 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.GeometryPlanes; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.QuadSplittingMode; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.FullTQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; +import org.joml.Vector3fc; /** * The BSP workspace holds the state during the BSP building process. (see also * BSPSortState) It brings a number of fixed parameters and receives partition * planes to return as part of the final result. - * + *

* Implementation note: Storing the multi partition node's interval points in a * global array instead of making a new one at each tree level doesn't appear to * have any performance benefit. */ -class BSPWorkspace { - /** - * All the quads in the section. - */ - final TQuad[] quads; - - final SectionPos sectionPos; - +class BSPWorkspace extends ObjectArrayList { final BSPResult result = new BSPResult(); - final boolean prepareNodeReuse; + private final SectionPos sectionPos; + boolean prepareNodeReuse; + final boolean allowNodeReuse; + final boolean quantizeTriggerNormals; - BSPWorkspace(TQuad[] quads, SectionPos sectionPos, boolean prepareNodeReuse) { - this.quads = quads; + private int quadCount; + private final int maxQuadCount; + private IntArrayList availableQuadIndexes; + private UpdatedQuadsList updatedQuads; + + BSPWorkspace(TQuad[] quads, SectionPos sectionPos, boolean prepareNodeReuse, boolean allowNodeReuse, QuadSplittingMode quadSplittingMode) { + super(quads); this.sectionPos = sectionPos; this.prepareNodeReuse = prepareNodeReuse; + this.allowNodeReuse = allowNodeReuse; + this.quantizeTriggerNormals = quadSplittingMode.quantizeTriggerNormals(); + + this.quadCount = quads.length; + if (quadSplittingMode.allowsSplitting()) { + this.maxQuadCount = quadSplittingMode.getMaxTotalQuads(this.quadCount); + } else { + this.maxQuadCount = this.quadCount; + } + } + + boolean canSplitQuads() { + return this.quadCount < this.maxQuadCount; } // TODO: better bidirectional triggering: integrate bidirectionality in GFNI if // top-level topo sorting isn't used anymore (and only use half as much memory - // by not storing it double) + // by not storing trigger planes twice) void addAlignedPartitionPlane(int axis, float distance) { - result.addDoubleSidedPlane(this.sectionPos, axis, distance); + this.result.addDoubleSidedAlignedPlane(this.sectionPos, axis, distance); + } + + void addUnalignedPartitionPlane(Vector3fc planeNormal, float distance) { + this.result.addDoubleSidedUnalignedPlane(this.sectionPos, planeNormal, distance); + } + + private void registerQuadUpdate(FullTQuad quad) { + if (quad.triggerAndSetUpdatedVertices()) { + if (this.updatedQuads == null) { + this.updatedQuads = new UpdatedQuadsList(); + } + this.updatedQuads.add(quad); + } + + // don't attempt any node reuse preparation if any quads were split, + // already prepared node reuse will simply be ignored + this.prepareNodeReuse = false; + } + + public UpdatedQuadsList getFinalizedUpdatedQuads() { + if (this.updatedQuads != null) { + this.updatedQuads.setQuadCounts(this.size(), this.quadCount); + } + return this.updatedQuads; + } + + int pushQuad(FullTQuad quad) { + // null or invalid quads simply don't get added + if (quad == null || quad.isInvalid()) { + return -1; + } + + // take an index from the list of holes if there are any + int index; + if (this.availableQuadIndexes == null || this.availableQuadIndexes.isEmpty()) { + index = this.size(); + this.add(quad); + } else { + index = this.availableQuadIndexes.removeInt(this.availableQuadIndexes.size() - 1); + this.set(index, quad); + } + + quad.setWriteToIndex(index); + this.quadCount++; + + this.registerQuadUpdate(quad); + + return index; + } + + int updateQuad(FullTQuad quad, int quadIndex) { + if (quad == null) { + return -1; + } + + // invalid quads that have already been added to this list have to be removed + if (quad.isInvalid()) { + var lastIndex = this.size() - 1; + if (quadIndex == lastIndex) { + this.remove(lastIndex); + } else { + this.set(quadIndex, null); + if (this.availableQuadIndexes == null) { + this.availableQuadIndexes = new IntArrayList(); + } + this.availableQuadIndexes.add(quadIndex); + } + + quad.setNoWrite(); + this.registerQuadUpdate(quad); + + this.quadCount--; + + return -1; + } + + quad.setWriteToIndex(quadIndex); + this.registerQuadUpdate(quad); + + return quadIndex; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerBinaryPartitionBSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerBinaryPartitionBSPNode.java index 4151684714..cb84af2272 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerBinaryPartitionBSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerBinaryPartitionBSPNode.java @@ -1,8 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; -import org.joml.Vector3fc; - import it.unimi.dsi.fastutil.ints.IntArrayList; +import org.joml.Vector3fc; /** * Partitions quads into two sides, each its own BSP node, of a partition plane @@ -17,7 +16,7 @@ class InnerBinaryPartitionBSPNode extends InnerPartitionBSPNode { private final int[] onPlaneQuads; InnerBinaryPartitionBSPNode(NodeReuseData reuseData, float planeDistance, int axis, - BSPNode inside, BSPNode outside, int[] onPlaneQuads) { + BSPNode inside, BSPNode outside, int[] onPlaneQuads) { super(reuseData, axis); this.planeDistance = planeDistance; this.inside = inside; @@ -25,9 +24,22 @@ class InnerBinaryPartitionBSPNode extends InnerPartitionBSPNode { this.onPlaneQuads = onPlaneQuads; } + InnerBinaryPartitionBSPNode(NodeReuseData reuseData, float planeDistance, Vector3fc planeNormal, + BSPNode inside, BSPNode outside, int[] onPlaneQuads) { + super(reuseData, planeNormal); + this.planeDistance = planeDistance; + this.inside = inside; + this.outside = outside; + this.onPlaneQuads = onPlaneQuads; + } + @Override void addPartitionPlanes(BSPWorkspace workspace) { - workspace.addAlignedPartitionPlane(this.axis, this.planeDistance); + if (this.axis == UNALIGNED_AXIS) { + workspace.addUnalignedPartitionPlane(this.planeNormal, this.planeDistance); + } else { + workspace.addAlignedPartitionPlane(this.axis, this.planeDistance); + } // also add the planes of the children if (this.inside instanceof InnerPartitionBSPNode insideChild) { @@ -99,4 +111,39 @@ static BSPNode buildFromPartitions(BSPWorkspace workspace, IntArrayList indexes, partitionDistance, axis, insideNode, outsideNode, onPlane); } + + static BSPNode buildFromParts(BSPWorkspace workspace, IntArrayList indexes, int depth, BSPNode oldNode, + IntArrayList inside, IntArrayList outside, IntArrayList onPlane, int axis, Vector3fc planeNormal, float partitionDistance) { + if (axis == UNALIGNED_AXIS) { + workspace.addUnalignedPartitionPlane(planeNormal, partitionDistance); + } else { + workspace.addAlignedPartitionPlane(axis, Math.abs(partitionDistance)); + } + + BSPNode oldInsideNode = null; + BSPNode oldOutsideNode = null; + if (oldNode instanceof InnerBinaryPartitionBSPNode binaryNode && + binaryNode.axis == axis && + (axis == UNALIGNED_AXIS || binaryNode.planeNormal.equals(planeNormal)) && + binaryNode.planeDistance == partitionDistance) { + oldInsideNode = binaryNode.inside; + oldOutsideNode = binaryNode.outside; + } + + BSPNode insideNode = null; + BSPNode outsideNode = null; + if (inside != null) { + insideNode = BSPNode.build(workspace, inside, depth, oldInsideNode); + } + if (outside != null) { + outsideNode = BSPNode.build(workspace, outside, depth, oldOutsideNode); + } + var onPlaneArr = BSPSortState.compressIndexes(onPlane); + + // always use the correct plane normal here because just specifying the axis causes the constructor to use a wrong and unsigned normal + return new InnerBinaryPartitionBSPNode( + prepareNodeReuse(workspace, indexes, depth), + partitionDistance, planeNormal, + insideNode, outsideNode, onPlaneArr); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerMultiPartitionBSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerMultiPartitionBSPNode.java index cabcc659bc..38a02885b7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerMultiPartitionBSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerMultiPartitionBSPNode.java @@ -1,15 +1,14 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; -import org.joml.Vector3fc; - import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import org.joml.Vector3fc; /** * Partitions quads into multiple child BSP nodes with multiple parallel * partition planes. This is uses less memory and time than constructing a * binary BSP tree through more partitioning passes. - * + *

* Implementation note: Detecting and avoiding the double array when possible * brings no performance benefit in sorting speed, only a building speed * detriment. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java index 3ee6572093..66fe1588cd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java @@ -6,18 +6,25 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TopoGraphSorting; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.FullTQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.sorting.RadixSort; import net.minecraft.util.Mth; +import org.joml.Vector3f; import org.joml.Vector3fc; import java.util.Arrays; import java.util.Random; +import static net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder.Vertex.copyVertexTo; +import static net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder.Vertex.writeVertex; + /** * Performs aligned BSP partitioning of many nodes and constructs appropriate * BSP nodes based on the result. @@ -52,6 +59,7 @@ abstract class InnerPartitionBSPNode extends BSPNode { private static final int NODE_REUSE_THRESHOLD = 30; private static final int MAX_INTERSECTION_ATTEMPTS = 500; + protected static final int UNALIGNED_AXIS = -1; final Vector3fc planeNormal; final int axis; @@ -71,7 +79,7 @@ abstract class InnerPartitionBSPNode extends BSPNode { * Since the indexes might be compressed, the count needs to be stored * separately from before compression. */ - record NodeReuseData(float[][] quadExtents, int[] indexes, int indexCount, int maxIndex) { + record NodeReuseData(int quadHash, int[] indexes, int indexCount, int maxIndex) { } InnerPartitionBSPNode(NodeReuseData reuseData, int axis) { @@ -80,6 +88,12 @@ record NodeReuseData(float[][] quadExtents, int[] indexes, int indexCount, int m this.reuseData = reuseData; } + InnerPartitionBSPNode(NodeReuseData reuseData, Vector3fc planeNormal) { + this.planeNormal = planeNormal; + this.axis = UNALIGNED_AXIS; + this.reuseData = reuseData; + } + abstract void addPartitionPlanes(BSPWorkspace workspace); static NodeReuseData prepareNodeReuse(BSPWorkspace workspace, IntArrayList indexes, int depth) { @@ -87,20 +101,19 @@ static NodeReuseData prepareNodeReuse(BSPWorkspace workspace, IntArrayList index // root node and not anything deeper than its children) if (workspace.prepareNodeReuse && depth == 1 && indexes.size() > NODE_REUSE_THRESHOLD) { // collect the extents of the indexed quads and hash them - var quadExtents = new float[indexes.size()][]; + var quadHash = 1; int maxIndex = -1; for (int i = 0; i < indexes.size(); i++) { var index = indexes.getInt(i); - var quad = workspace.quads[index]; - var extents = quad.getExtents(); - quadExtents[i] = extents; + var quad = workspace.get(index); + quadHash = quadHash * 31 + quad.getQuadHash(); maxIndex = Math.max(maxIndex, index); } // compress indexes but without sorting them, as the order needs to be the same // for the extents comparison loop to work return new NodeReuseData( - quadExtents, + quadHash, BSPSortState.compressIndexes(indexes, false), indexes.size(), maxIndex); @@ -140,7 +153,7 @@ boolean hasFixedOffset() { } static InnerPartitionBSPNode attemptNodeReuse(BSPWorkspace workspace, IntArrayList newIndexes, InnerPartitionBSPNode oldNode) { - if (oldNode == null) { + if (oldNode == null || !workspace.allowNodeReuse) { return null; } @@ -152,15 +165,18 @@ static InnerPartitionBSPNode attemptNodeReuse(BSPWorkspace workspace, IntArrayLi return null; } - var oldExtents = reuseData.quadExtents; - if (oldExtents.length != newIndexes.size()) { + if (reuseData.indexCount != newIndexes.size()) { return null; } + var newQuadHash = 1; for (int i = 0; i < newIndexes.size(); i++) { - if (!workspace.quads[newIndexes.getInt(i)].extentsEqual(oldExtents[i])) { - return null; - } + var index = newIndexes.getInt(i); + var quad = workspace.get(index); + newQuadHash = newQuadHash * 31 + quad.getQuadHash(); + } + if (newQuadHash != reuseData.quadHash) { + return null; } // reuse old node and either apply a fixed offset or calculate an index map to @@ -236,17 +252,28 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS ReferenceArrayList partitions = new ReferenceArrayList<>(); LongArrayList points = new LongArrayList((int) (indexes.size() * 1.5)); + // keep track of global best splitting group for splitting quads if enabled + IntArrayList bestSplittingGroup = null; + IntArrayList splittingGroup = null; + boolean canSplitQuads = workspace.canSplitQuads(); + if (canSplitQuads) { + bestSplittingGroup = new IntArrayList(5); + splittingGroup = new IntArrayList(5); + } + // find any aligned partition, search each axis for (int axisCount = 0; axisCount < 3; axisCount++) { int axis = (axisCount + depth + 1) % 3; var oppositeDirection = axis + 3; int alignedFacingBitmap = 0; boolean onlyIntervalSide = true; + int positiveSignCount = 0; // collect all the geometry's start and end points in this direction points.clear(); - for (int quadIndex : indexes) { - var quad = workspace.quads[quadIndex]; + for (int i = 0, size = indexes.size(); i < size; i++) { + int quadIndex = indexes.getInt(i); + var quad = workspace.get(quadIndex); var extents = quad.getExtents(); var posExtent = extents[axis]; var negExtent = extents[oppositeDirection]; @@ -258,7 +285,11 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS onlyIntervalSide = false; } - alignedFacingBitmap |= 1 << quad.getFacing().ordinal(); + var facing = quad.getFacing(); + if (facing.getSign() > 0) { + positiveSignCount++; + } + alignedFacingBitmap |= 1 << facing.ordinal(); } // simplified SNR heuristic as seen in TranslucentGeometryCollector#sortTypeHeuristic (case D) @@ -271,9 +302,9 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS // check if the geometry is aligned to the axis if (onlyIntervalSide) { // this means the already generated points array can be used - return buildSNRLeafNodeFromPoints(workspace, points); + return buildSNRLeafNodeFromPoints(workspace, points, positiveSignCount); } else { - return buildSNRLeafNodeFromQuads(workspace, indexes, points); + return buildSNRLeafNodeFromQuads(workspace, indexes); } } } @@ -282,13 +313,27 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS // longs directly has the same effect because of the encoding. Arrays.sort(points.elements(), 0, points.size()); - // find gaps - partitions.clear(); + // the current partition plane distance (dot product), + // updated when an interval ends or a side is encountered, used to add quads to quadsOn float distance = Float.NaN; + + // set of quads that are within the partition IntArrayList quadsBefore = null; + + // set of quads that are on the partition plane IntArrayList quadsOn = null; + + // number of overlapping intervals along the projection axis int thickness = 0; - for (long point : points) { + + // lazily generate partitions by keeping track of the current interval thickness and the quads that are on the partition plane + partitions.clear(); + if (canSplitQuads) { + splittingGroup.clear(); + } + float splitDistance = Float.NaN; + for (int i = 0, size = points.size(); i < size; i++) { + long point = points.getLong(i); switch (decodeType(point)) { case INTERVAL_START -> { // unless at the start, flush if there's a gap @@ -339,18 +384,32 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS } quadsOn.add(pointQuadIndex); } else { + // add this point quad to the quads before the partition plane if (quadsBefore == null) { throw new IllegalStateException("there must be started intervals here"); } quadsBefore.add(pointQuadIndex); - if (quadsOn == null) { - distance = decodeDistance(point); + + // update the splitting group if the distance didn't change + if (canSplitQuads) { + var ownDistance = decodeDistance(point); + if (ownDistance == splitDistance || Float.isNaN(splitDistance)) { + splittingGroup.add(pointQuadIndex); + } else { + flushBestSplittingGroup(splittingGroup, bestSplittingGroup, axis); + } + splitDistance = ownDistance; } } } } } + // check if the splitting group needs to be flushed + if (canSplitQuads) { + flushBestSplittingGroup(splittingGroup, bestSplittingGroup, axis); + } + // check a different axis if everything is in one quadsBefore, // which means there are no gaps if (quadsBefore != null && quadsBefore.size() == indexes.size()) { @@ -384,17 +443,451 @@ static BSPNode build(BSPWorkspace workspace, IntArrayList indexes, int depth, BS partitions, axis, endsWithPlane); } - var intersectingHandling = handleIntersecting(workspace, indexes, depth, oldNode); - if (intersectingHandling != null) { - return intersectingHandling; + if (canSplitQuads) { + // try static topo sorting first because splitting quads is even more expensive + var multiLeafNode = buildTopoMultiLeafNode(workspace, indexes, true); + if (multiLeafNode != null) { + return multiLeafNode; + } + + // perform quad splitting to get a sortable result whether it's intersecting or just unsortable as-is + return handleUnsortableBySplitting(workspace, indexes, depth, oldNode, bestSplittingGroup); + } else { + var intersectingHandling = handleIntersecting(workspace, indexes, depth, oldNode); + if (intersectingHandling != null) { + return intersectingHandling; + } + + // attempt topo sorting on the geometry if intersection handling failed + var multiLeafNode = buildTopoMultiLeafNode(workspace, indexes, false); + if (multiLeafNode == null) { + throw new BSPBuildFailureException("No partition found but not intersecting and can't be statically topo sorted"); + } + return multiLeafNode; + } + } + + static void flushBestSplittingGroup(IntArrayList splittingGroup, IntArrayList bestSplittingGroup, int axis) { + var currentSize = splittingGroup.size(); + var newSize = bestSplittingGroup.size(); + + // use new splitting group if it's bigger but prefer y-axis for splitting if they're the same size + if (currentSize > newSize || currentSize == newSize && axis == 1) { + bestSplittingGroup.clear(); + bestSplittingGroup.addAll(splittingGroup); + } + splittingGroup.clear(); + } + + private static boolean floatEquals(float a, float b) { + return Float.floatToIntBits(a) == Float.floatToIntBits(b) || Math.abs(a - b) <= TQuad.VERTEX_EPSILON; + } + + static private BSPNode handleUnsortableBySplitting(BSPWorkspace workspace, IntArrayList indexes, int depth, BSPNode oldNode, IntArrayList splittingGroup) { + // pick the first quad if there's no prepared splitting group + int representativeIndex; + if (splittingGroup.isEmpty()) { + representativeIndex = indexes.getInt(0); + splittingGroup.add(representativeIndex); + } else { + representativeIndex = splittingGroup.getInt(0); + } + var representative = (FullTQuad) workspace.get(representativeIndex); + var representativeFacing = representative.getFacing(); + int initialSplittingGroupSize = splittingGroup.size(); + + // split all quads by the splitting group's plane + var splitPlane = representative.getVeryAccurateNormal(); + var splitDistance = representative.getAccurateDotProduct(); + var splitPlaneNeg = splitPlane.negate(new Vector3f()); + var splitDistanceNeg = -splitDistance; + var splitPlaneIsAligned = representativeFacing.isAligned(); + + IntArrayList inside = new IntArrayList(); + IntArrayList outside = new IntArrayList(); + + for (int candidateIndex : indexes) { + // eliminate quads that are already in the splitting group + var isInSplittingGroup = false; + for (int i = 0; i < initialSplittingGroupSize; i++) { + if (candidateIndex == splittingGroup.getInt(i)) { + isInSplittingGroup = true; + } + } + if (isInSplittingGroup) { + continue; + } + + var insideQuad = (FullTQuad) workspace.get(candidateIndex); + var quadFacing = insideQuad.getFacing(); + + // eliminate quads that lie in the split plane + if (quadFacing == representativeFacing) { + var accurateNormal = insideQuad.getVeryAccurateNormal(); + var accurateDotProduct = insideQuad.getAccurateDotProduct(); + var coplanar = floatEquals(accurateDotProduct, splitDistance) && (splitPlaneIsAligned || + accurateNormal.equals(splitPlane, TQuad.VERTEX_EPSILON)); + var antiCoplanar = coplanar || floatEquals(accurateDotProduct, splitDistanceNeg) && (splitPlaneIsAligned || + accurateNormal.equals(splitPlaneNeg, TQuad.VERTEX_EPSILON)); + if (coplanar || antiCoplanar) { + splittingGroup.add(candidateIndex); + continue; + } + } + + // split the geometry with the plane + splitCandidate(workspace, splittingGroup, candidateIndex, insideQuad, splitPlane, splitDistance, outside, inside); + } + + ModelQuadFacing facing; + Vector3fc normal; + float dotProduct; + if (workspace.quantizeTriggerNormals) { + facing = representative.useQuantizedFacing(); + normal = representative.getQuantizedNormal(); + dotProduct = representative.getQuantizedDotProduct(); + } else { + facing = representativeFacing; + normal = splitPlane; + dotProduct = splitDistance; + } + int axis = UNALIGNED_AXIS; + if (facing.isAligned()) { + axis = facing.getAxis(); + } + + return InnerBinaryPartitionBSPNode.buildFromParts( + workspace, indexes, depth, oldNode, inside, outside, splittingGroup, axis, + normal, dotProduct); + } + + private static void splitCandidate(BSPWorkspace workspace, IntArrayList splittingGroup, int candidateIndex, FullTQuad insideQuad, Vector3fc splitPlane, float splitDistance, IntArrayList outside, IntArrayList inside) { + // Lines or points (2 or 1 vertices) should have been filtered out + var uniqueVertexMap = insideQuad.getUniqueVertexMap(); + var uniqueVertices = Integer.bitCount(uniqueVertexMap); + if (uniqueVertices < 3) { + throw new IllegalStateException("Unexpected quad with less than 3 unique vertices"); + } + + var vertices = insideQuad.getVertices(); + + // calculate inside/outside for each vertex + int insideMapUnmasked = 0; + int onPlaneMapUnmasked = 0; + for (int i = 0; i < 4; i++) { + var vertex = vertices[i]; + var dot = splitPlane.dot(vertex.x, vertex.y, vertex.z); + var delta = dot - splitDistance; + if (Math.abs(delta) < TQuad.VERTEX_EPSILON) { + onPlaneMapUnmasked |= 1 << i; + } else if (delta < 0) { // dot < splitDistance + insideMapUnmasked |= 1 << i; + } + } + + // filter out the vertices that are duplicated to handle triangles + var insideMap = insideMapUnmasked & uniqueVertexMap; + var onPlaneMap = onPlaneMapUnmasked & uniqueVertexMap; + + // Quads or triangles that are fully on the plane are added to the splitting group. + // Bent quads are not dealt with by this and will simply produce unexpected behavior. + // Adding quads with three unique vertices on the plane to the splitting group doesn't work when floating point errors + // cause quads to have effectively three unique vertices without signaling so in their uniqueVertexMap. + if (onPlaneMap == uniqueVertexMap) { + splittingGroup.add(candidateIndex); + return; + } + + // the quad is outside if all vertices are either outside or on the plane + if (insideMap == 0) { + outside.add(candidateIndex); + return; + } + + // the quad is inside if all vertices are either inside or on the plane + if ((insideMap | onPlaneMap) == uniqueVertexMap) { // 0b1111 for quads + inside.add(candidateIndex); + return; + } + + var onPlaneCount = Integer.bitCount(onPlaneMap); + var insideCount = Integer.bitCount(insideMap); + + // cancel splitting after handling special cases if the new geometry limit has been reached + if (!workspace.canSplitQuads()) { + // put on, inside, outside based on which side has the most vertices + var outsideCount = 4 - insideCount - onPlaneCount; + if (onPlaneCount >= insideCount && onPlaneCount >= outsideCount) { + splittingGroup.add(candidateIndex); + } else if (insideCount >= outsideCount) { + inside.add(candidateIndex); + } else { + outside.add(candidateIndex); + } + return; + } + + FullTQuad outsideQuad = FullTQuad.splittingCopy(insideQuad); + FullTQuad secondOutsideQuad = null; + FullTQuad secondInsideQuad = null; + + if (uniqueVertices == 3) { + // TODO: deal with the rare and weird case where opposite vertices are identical (i.e. the quad is folded in half) + + // a vertex is on the split plane + var sameVertexMap = insideQuad.getSameVertexMap(); + if (onPlaneCount == 1) { + int duplicateIndex = -1; + boolean duplicateIsInside = false; + + // the duplicate vertex is inside or outside, find its index and side. + // if the duplicate vertex is on the plane, don't move it. + if ((onPlaneMapUnmasked & sameVertexMap) == 0) { + duplicateIsInside = (sameVertexMap & insideMapUnmasked) != 0; + duplicateIndex = Integer.numberOfTrailingZeros(sameVertexMap); + } + + var insideIndex = Integer.numberOfTrailingZeros(insideMap); + var outsideIndex = Integer.numberOfTrailingZeros(~(insideMap | onPlaneMap) & uniqueVertexMap); + splitTriangleVertex(insideIndex, outsideIndex, duplicateIndex, duplicateIsInside, insideQuad, outsideQuad, splitPlane, splitDistance); + } + + // even splitting if the two equal vertices are the corner that's being split off. + // at this point onPlaneCount == 0 + else if (Integer.bitCount(insideMapUnmasked) == 2) { + splitQuadEven(insideMapUnmasked, insideQuad, outsideQuad, splitPlane, splitDistance); + } + + // a single vertex is inside or outside (with the other three, including one duplicate, being on the other side) + else if (insideCount == 1) { + var cornerIndex = Integer.numberOfTrailingZeros(insideMap); + splitTriangleCorner(cornerIndex, insideQuad, outsideQuad, splitPlane, splitDistance); + } else { + var cornerIndex = Integer.numberOfTrailingZeros(~insideMapUnmasked); + splitTriangleCorner(cornerIndex, outsideQuad, insideQuad, splitPlane, splitDistance); + } + } else { // uniqueVertices == 4, masked == unmasked + // it's split along the diagonal. + // this case can be treated like even splitting if the two on-plane vertices are declared as each part of one of the sides + if (onPlaneCount == 2 && onPlaneMap == 0b0101) { + insideMap |= 0b0001; + insideCount = 2; + } else if (onPlaneCount == 2 && onPlaneMap == 0b1010) { + insideMap |= 0b0010; + insideCount = 2; + } + + // or it's a bent quad where one edge lies on the split and the opposite edge crosses the split. + // in this case it simply falls through to one of the odd splitting modes + + // one vertex being on the plane now implies the quad is split on a vertex and through an edge. + // if there is one vertex inside (and two outside), move the on-plane vertex inside to produce an even split case. + // in the other case nothing needs to be done since for splitting the 0-bits in the insideMap are treated as outside. + else if (onPlaneCount == 1 && insideCount == 1) { + insideMap |= onPlaneMap; + insideCount = 2; + } + + // split evenly with two quads or three quads (corner chopped off) depending on the orientation + if (insideCount == 2) { + splitQuadEven(insideMap, insideQuad, outsideQuad, splitPlane, splitDistance); + } else if (insideCount == 3) { + var cornerIndex = Integer.numberOfTrailingZeros(~insideMap); + secondInsideQuad = FullTQuad.splittingCopy(insideQuad); + + splitQuadOdd(cornerIndex, outsideQuad, secondInsideQuad, insideQuad, splitPlane, splitDistance); + } else { // insideCount == 1 + var cornerIndex = Integer.numberOfTrailingZeros(insideMap); + secondOutsideQuad = FullTQuad.splittingCopy(insideQuad); + + splitQuadOdd(cornerIndex, insideQuad, secondOutsideQuad, outsideQuad, splitPlane, splitDistance); + } + } + + addQuadIndex(inside, workspace.updateQuad(insideQuad, candidateIndex)); + addQuadIndex(outside, workspace.pushQuad(outsideQuad)); + addQuadIndex(inside, workspace.pushQuad(secondInsideQuad)); + addQuadIndex(outside, workspace.pushQuad(secondOutsideQuad)); + } + + static private void addQuadIndex(IntArrayList list, int index) { + if (index >= 0) { + list.add(index); } + } + + static private void splitQuadEven(int vertexInsideMap, FullTQuad insideQuad, FullTQuad outsideQuad, Vector3fc splitPlane, float splitDistance) { + // the quad is split through two of its opposing edges, producing two regular quads + + // split the quad with the plane by iterating all the edges and checking for intersection + var insideVertices = insideQuad.getVertices(); + var outsideVertices = outsideQuad.getVertices(); + for (int indexA = 0; indexA < 4; indexA++) { + var indexB = (indexA + 1) & 0b11; + var insideA = (vertexInsideMap & (1 << indexA)) != 0; + var insideB = (vertexInsideMap & (1 << indexB)) != 0; + if (insideA == insideB) { + continue; + } - // attempt topo sorting on the geometry if intersection handling failed - var multiLeafNode = buildTopoMultiLeafNode(workspace, indexes); - if (multiLeafNode == null) { - throw new BSPBuildFailureException("No partition found but not intersecting and can't be statically topo sorted"); + // get the inner and outer vertices + int insideIndex, outsideIndex; + if (insideA) { + insideIndex = indexA; + outsideIndex = indexB; + } else { + insideIndex = indexB; + outsideIndex = indexA; + } + + interpolateAttributes(splitDistance, splitPlane, + insideVertices[insideIndex], outsideVertices[outsideIndex], + insideVertices[outsideIndex], outsideVertices[insideIndex]); + } + + insideQuad.updateSplitQuadAfterVertexModification(); + outsideQuad.updateSplitQuadAfterVertexModification(); + } + + static private void splitQuadOdd(int cornerIndex, FullTQuad cornerQuad, FullTQuad cutQuad, FullTQuad bulkQuad, Vector3fc splitPlane, float splitDistance) { + // the quad is split through two of its adjacent edges, producing three quads (two triangles and one quad) + + var cornerVertices = cornerQuad.getVertices(); // corner split off by the plane + var cutVertices = cutQuad.getVertices(); // quad between the corner and the bulk + var bulkVertices = bulkQuad.getVertices(); // quad that retains the three non-cut vertices + + var prevIndex = (cornerIndex - 1) & 0b11; + var nextIndex = (cornerIndex + 1) & 0b11; + var oppositeIndex = (cornerIndex + 2) & 0b11; + + // inverting the split plane based on whether the corner is inside or outside doesn't seem to be necessary, + // because it just works out in the interpolation, and the negative values cancel out + + var cornerVertex = cornerVertices[cornerIndex]; + + interpolateAttributes(splitDistance, splitPlane, + cornerVertex, bulkVertices[nextIndex], + cornerVertices[nextIndex], cutVertices[nextIndex], bulkVertices[cornerIndex]); + interpolateAttributes(splitDistance, splitPlane, + cornerVertex, bulkVertices[prevIndex], + cornerVertices[prevIndex], cornerVertices[oppositeIndex], cutVertices[cornerIndex]); + copyVertexTo(cutVertices[prevIndex], cutVertices[oppositeIndex]); + + cornerQuad.updateSplitQuadAfterVertexModification(); + cutQuad.updateSplitQuadAfterVertexModification(); + bulkQuad.updateSplitQuadAfterVertexModification(); + } + + static private void splitTriangleCorner(int cornerIndex, FullTQuad cornerQuad, FullTQuad bulkQuad, Vector3fc splitPlane, float splitDistance) { + // the triangle (degenerate quad) is split through two edges, producing two quads (one triangle and one quad) + + var cornerVertices = cornerQuad.getVertices(); // corner split off by the plane + var bulkVertices = bulkQuad.getVertices(); // quad that retains the other vertices + + var prevIndex = (cornerIndex - 1) & 0b11; + var nextIndex = (cornerIndex + 1) & 0b11; + var oppositeIndex = (cornerIndex + 2) & 0b11; + + var cornerVertex = cornerVertices[cornerIndex]; + + interpolateAttributes(splitDistance, splitPlane, + cornerVertex, bulkVertices[nextIndex], + cornerVertices[nextIndex], cornerVertices[oppositeIndex], bulkVertices[cornerIndex]); + copyVertexTo(bulkVertices[prevIndex], bulkVertices[oppositeIndex]); + interpolateAttributes(splitDistance, splitPlane, + cornerVertex, bulkVertices[prevIndex], + cornerVertices[prevIndex], bulkVertices[prevIndex]); + + cornerQuad.updateSplitQuadAfterVertexModification(); + bulkQuad.updateSplitQuadAfterVertexModification(); + } + + static private void splitTriangleVertex(int insideIndex, int outsideIndex, int duplicateIndex, boolean duplicateIsInside, FullTQuad insideQuad, FullTQuad outsideQuad, Vector3fc splitPlane, float splitDistance) { + // the triangle (degenerate quad) is split through one edge, producing two triangles + + var insideVertices = insideQuad.getVertices(); + var outsideVertices = outsideQuad.getVertices(); + + // the duplicate vertex of the opposite quad is moved to the center too + ChunkVertexEncoder.Vertex duplicateTarget = null; + if (duplicateIndex != -1) { + if (duplicateIsInside) { + duplicateTarget = outsideVertices[duplicateIndex]; + } else { + duplicateTarget = insideVertices[duplicateIndex]; + } + } + + interpolateAttributes(splitDistance, splitPlane, + insideVertices[insideIndex], outsideVertices[outsideIndex], + insideVertices[outsideIndex], outsideVertices[insideIndex], duplicateTarget); + + insideQuad.updateSplitQuadAfterVertexModification(); + outsideQuad.updateSplitQuadAfterVertexModification(); + } + + private static void interpolateAttributes(float splitDistance, Vector3fc splitPlane, ChunkVertexEncoder.Vertex inside, ChunkVertexEncoder.Vertex outside, ChunkVertexEncoder.Vertex targetA, ChunkVertexEncoder.Vertex targetB) { + interpolateAttributes(splitDistance, splitPlane, inside, outside, targetA, targetB, null); + } + + private static void interpolateAttributes(float splitDistance, Vector3fc splitPlane, ChunkVertexEncoder.Vertex inside, ChunkVertexEncoder.Vertex outside, ChunkVertexEncoder.Vertex targetA, ChunkVertexEncoder.Vertex targetB, ChunkVertexEncoder.Vertex targetC) { + // calculate the intersection point and interpolate attributes + var insideToOutsideX = outside.x - inside.x; + var insideToOutsideY = outside.y - inside.y; + var insideToOutsideZ = outside.z - inside.z; + + // use an epsilon in this check to prevent splitPlaneEdgeDot from being zero when a very small insideToOutside_ vanishes in the dot product + if (Math.abs(insideToOutsideX) < TQuad.VERTEX_EPSILON && + Math.abs(insideToOutsideY) < TQuad.VERTEX_EPSILON && + Math.abs(insideToOutsideZ) < TQuad.VERTEX_EPSILON) { + copyVertexToMultiple(inside, targetA, targetB, targetC); + return; + } + + var splitPlaneEdgeDot = splitPlane.dot(insideToOutsideX, insideToOutsideY, insideToOutsideZ); + + // the edge lies within the split plane if the dot product is zero + if (splitPlaneEdgeDot == 0) { + // this should never happen because we handle triangles correctly + throw new IllegalStateException("Quad with an edge in the split plane should have been handled earlier"); + } + + var outsideAmount = (splitDistance - splitPlane.dot(inside.x, inside.y, inside.z)) / splitPlaneEdgeDot; + + if (outsideAmount >= 1) { + copyVertexToMultiple(outside, targetA, targetB, targetC); + return; + } else if (outsideAmount <= 0) { + copyVertexToMultiple(inside, targetA, targetB, targetC); + return; + } + + var newX = inside.x + insideToOutsideX * outsideAmount; + var newY = inside.y + insideToOutsideY * outsideAmount; + var newZ = inside.z + insideToOutsideZ * outsideAmount; + + var newColor = ColorMixer.mix(inside.color, outside.color, outsideAmount); + var newAo = Mth.lerp(outsideAmount, inside.ao, outside.ao); + var newU = Mth.lerp(outsideAmount, inside.u, outside.u); + var newV = Mth.lerp(outsideAmount, inside.v, outside.v); + + var newLightBl = Mth.lerp(outsideAmount, inside.light & 0xFF, outside.light & 0xFF); + var newLightSl = Mth.lerp(outsideAmount, inside.light >> 16, outside.light >> 16); + var newLight = (((int) newLightSl & 0xFF) << 16) | ((int) newLightBl & 0xFF); + + writeVertex(targetA, newX, newY, newZ, newColor, newAo, newU, newV, newLight); + writeVertex(targetB, newX, newY, newZ, newColor, newAo, newU, newV, newLight); + if (targetC != null) { + writeVertex(targetC, newX, newY, newZ, newColor, newAo, newU, newV, newLight); + } + } + + private static void copyVertexToMultiple(ChunkVertexEncoder.Vertex from, ChunkVertexEncoder.Vertex targetA, ChunkVertexEncoder.Vertex targetB, ChunkVertexEncoder.Vertex targetC) { + copyVertexTo(from, targetA); + copyVertexTo(from, targetB); + if (targetC != null) { + copyVertexTo(from, targetC); } - return multiLeafNode; } static private BSPNode handleIntersecting(BSPWorkspace workspace, IntArrayList indexes, int depth, BSPNode oldNode) { @@ -435,8 +928,8 @@ static private BSPNode handleIntersecting(BSPWorkspace workspace, IntArrayList i break; } - var quadA = workspace.quads[indexes.getInt(i)]; - var quadB = workspace.quads[indexes.getInt(j)]; + var quadA = workspace.get(indexes.getInt(i)); + var quadB = workspace.get(indexes.getInt(j)); // aligned quads intersect if their bounding boxes intersect if (TQuad.extentsIntersect(quadA, quadB)) { @@ -503,7 +996,7 @@ public void accept(int value) { } } - static private BSPNode buildTopoMultiLeafNode(BSPWorkspace workspace, IntArrayList indexes) { + static private BSPNode buildTopoMultiLeafNode(BSPWorkspace workspace, IntArrayList indexes, boolean failOnIntersection) { var quadCount = indexes.size(); if (quadCount > TranslucentGeometryCollector.STATIC_TOPO_UNKNOWN_FALLBACK_LIMIT) { @@ -514,12 +1007,12 @@ static private BSPNode buildTopoMultiLeafNode(BSPWorkspace workspace, IntArrayLi var activeToRealIndex = new int[quadCount]; for (int i = 0; i < indexes.size(); i++) { var quadIndex = indexes.getInt(i); - quads[i] = workspace.quads[quadIndex]; + quads[i] = workspace.get(quadIndex); activeToRealIndex[i] = quadIndex; } var indexWriter = new QuadIndexConsumerIntoArray(quadCount); - if (!TopoGraphSorting.topoGraphSort(indexWriter, quads, quads.length, activeToRealIndex, null, null)) { + if (!TopoGraphSorting.topoGraphSort(indexWriter, quads, quads.length, activeToRealIndex, null, null, failOnIntersection)) { return null; } @@ -529,63 +1022,60 @@ static private BSPNode buildTopoMultiLeafNode(BSPWorkspace workspace, IntArrayLi return new LeafMultiBSPNode(BSPSortState.compressIndexesInPlace(indexWriter.indexes, false)); } - static private BSPNode buildSNRLeafNodeFromQuads(BSPWorkspace workspace, IntArrayList indexes, LongArrayList points) { - // in this case the points array is wrong, but its allocation can be reused - - int[] quadIndexes; - - // adapted from SNR sorting code - if (RadixSort.useRadixSort(indexes.size())) { - final var keys = new int[indexes.size()]; - - for (int i = 0; i < indexes.size(); i++) { - var quadIndex = indexes.getInt(i); - keys[i] = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getAccurateDotProduct()); - } + static private BSPNode buildSNRLeafNodeFromQuads(BSPWorkspace workspace, IntArrayList indexes) { + final var indexBuffer = indexes.elements(); + final var indexCount = indexes.size(); - quadIndexes = RadixSort.sort(keys); + final var keys = new int[indexCount]; + final var perm = new int[indexCount]; - for (int i = 0; i < indexes.size(); i++) { - quadIndexes[i] = indexes.getInt(quadIndexes[i]); - } - } else { - final var sortData = points.elements(); + for (int i = 0; i < indexCount; i++) { + TQuad quad = workspace.get(indexBuffer[i]); + keys[i] = MathUtil.floatToComparableInt(quad.getAccurateDotProduct()); + perm[i] = i; + } - for (int i = 0; i < indexes.size(); i++) { - var quadIndex = indexes.getInt(i); - int dotProductComponent = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getAccurateDotProduct()); - sortData[i] = (long) dotProductComponent << 32 | quadIndex; - } + RadixSort.sortIndirect(perm, keys, true); - Arrays.sort(sortData, 0, indexes.size()); + for (int i = 0; i < indexCount; i++) { + perm[i] = indexBuffer[perm[i]]; + } - quadIndexes = new int[indexes.size()]; + return new LeafMultiBSPNode(BSPSortState.compressIndexes(IntArrayList.wrap(perm), false)); + } - for (int i = 0; i < indexes.size(); i++) { - quadIndexes[i] = (int) sortData[i]; + static private BSPNode buildSNRLeafNodeFromPoints(BSPWorkspace workspace, LongArrayList points, int positiveSignCount) { + int pointCount = points.size(); + if (positiveSignCount < pointCount) { + // invert the distance for all points where the quad is facing backwards, + // this is necessary to make the quad order stable relative to the quad index + for (int i = 0; i < pointCount; i++) { + // based one each quad's facing, order them forwards or backwards, + // this means forwards is written from the start and backwards is written from the end + var point = points.getLong(i); + var quadIndex = decodeQuadIndex(point); + if (workspace.get(quadIndex).getFacing().getSign() == -1) { + points.set(i, point ^ 0xFFFFFFFF00000000L); // invert distance bits + } } } - return new LeafMultiBSPNode(BSPSortState.compressIndexes(IntArrayList.wrap(quadIndexes), false)); - } - - static private BSPNode buildSNRLeafNodeFromPoints(BSPWorkspace workspace, LongArrayList points) { // also sort by ascending encoded point but then process as an SNR result - Arrays.sort(points.elements(), 0, points.size()); + Arrays.sort(points.elements(), 0, pointCount); // since the quads are aligned and are all INTERVAL_SIDE, there's no issues with duplicates. // the length of the array is exactly how many quads there are. - int[] quadIndexes = new int[points.size()]; - int forwards = 0; - int backwards = quadIndexes.length - 1; - for (int i = 0; i < points.size(); i++) { + int[] quadIndexes = new int[pointCount]; + int positive = 0; + int negative = positiveSignCount; + for (int i = 0; i < pointCount; i++) { // based one each quad's facing, order them forwards or backwards, // this means forwards is written from the start and backwards is written from the end var quadIndex = decodeQuadIndex(points.getLong(i)); - if (workspace.quads[quadIndex].getFacing().getSign() == 1) { - quadIndexes[forwards++] = quadIndex; + if (workspace.get(quadIndex).getFacing().getSign() == 1) { + quadIndexes[positive++] = quadIndex; } else { - quadIndexes[backwards--] = quadIndex; + quadIndexes[negative++] = quadIndex; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/UpdatedQuadsList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/UpdatedQuadsList.java new file mode 100644 index 0000000000..2291133710 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/UpdatedQuadsList.java @@ -0,0 +1,31 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree; + +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.FullTQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; + +import java.nio.ByteBuffer; + +public class UpdatedQuadsList extends ReferenceArrayList { + private int meshQuadCount; + private int indexQuadCount; + + public int getMeshQuadCount() { + return this.meshQuadCount; + } + + public int getIndexQuadCount() { + return this.indexQuadCount; + } + + public void setQuadCounts(int meshQuadCount, int indexQuadCount) { + this.meshQuadCount = meshQuadCount; + this.indexQuadCount = indexQuadCount; + } + + public void applyBufferUpdates(ChunkMeshBufferBuilder builder, ByteBuffer buffer) { + for (var quad : this) { + quad.writeToBuffer(builder, buffer); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/AnyOrderData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/AnyOrderData.java index 34f923ff02..0d3b1109c5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/AnyOrderData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/AnyOrderData.java @@ -1,7 +1,8 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; /** @@ -9,7 +10,7 @@ * order. However, they do need to be rendered with some index buffer, so that * vertices are assembled into quads. Since the sort order doesn't matter, all * sections with this sort type can share the same data in the index buffer. - * + *

* NOTE: A possible optimization would be to share the buffer for unordered * translucent sections on the CPU and on the GPU. It would essentially be the * same as SharedQuadIndexBuffer, but it has to be compatible with sections in @@ -18,11 +19,11 @@ * buffer segments and would need to be resized when a larger section wants to * use it. */ -public class AnyOrderData extends SplitDirectionData { +public class AnyOrderData extends PresentTranslucentData { private Sorter sorterOnce; - AnyOrderData(SectionPos sectionPos, int[] vertexCounts, int quadCount) { - super(sectionPos, vertexCounts, quadCount); + AnyOrderData(SectionPos sectionPos, int inputQuadCount) { + super(sectionPos, inputQuadCount); } @Override @@ -40,27 +41,18 @@ public Sorter getSorter() { return sorter; } + @Override + public boolean oldDataMatches(TranslucentGeometryCollector collector, SortType sortType, TQuad[] quads) { + // for the NONE sort type the ranges need to be the same, the actual geometry doesn't matter + return sortType == SortType.NONE && this.getInputQuadCount() == quads.length; + } + /** * Important: The vertex indexes must start at zero for each facing. */ - public static AnyOrderData fromMesh(int[] vertexCounts, - TQuad[] quads, SectionPos sectionPos) { - var anyOrderData = new AnyOrderData(sectionPos, vertexCounts, quads.length); - var sorter = new StaticSorter(quads.length); - anyOrderData.sorterOnce = sorter; - var indexBuffer = sorter.getIntBuffer(); - - for (var vertexCount : vertexCounts) { - if (vertexCount <= 0) { - continue; - } - - int count = TranslucentData.vertexCountToQuadCount(vertexCount); - for (int i = 0; i < count; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, i); - } - } - + public static AnyOrderData fromMesh(TQuad[] quads, SectionPos sectionPos) { + var anyOrderData = new AnyOrderData(sectionPos, quads.length); + anyOrderData.sorterOnce = new SharedIndexSorter(quads.length); return anyOrderData; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java index cfab09e721..ed2e9c64fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java @@ -1,28 +1,42 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.QuadSplittingMode; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.BSPNode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.BSPResult; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.UpdatedQuadsList; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; import org.joml.Vector3dc; /** * Constructs a BSP tree of the quads and sorts them dynamically. - * + *

* Triggering is performed when the BSP tree's partition planes are crossed in * any direction (bidirectional). */ public class DynamicBSPData extends DynamicData { private static final int NODE_REUSE_MIN_GENERATION = 1; + private final int indexQuadCount; private final BSPNode rootNode; private final int generation; + private UpdatedQuadsList updatedQuadsList; + private final boolean neededQuadSplitting; - private DynamicBSPData(SectionPos sectionPos, int vertexCount, BSPResult result, Vector3dc initialCameraPos, TQuad[] quads, int generation) { - super(sectionPos, vertexCount, quads.length, result, initialCameraPos); + private DynamicBSPData(SectionPos sectionPos, int inputQuadCount, BSPResult result, Vector3dc initialCameraPos, int generation) { + super(sectionPos, inputQuadCount, result, initialCameraPos); this.rootNode = result.getRootNode(); this.generation = generation; + this.updatedQuadsList = result.getUpdatedQuadsList(); + this.neededQuadSplitting = this.updatedQuadsList != null; + + if (this.updatedQuadsList != null) { + this.indexQuadCount = this.updatedQuadsList.getIndexQuadCount(); + } else { + this.indexQuadCount = inputQuadCount; + } } private class DynamicBSPSorter extends DynamicSorter { @@ -37,27 +51,55 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { } @Override - public Sorter getSorter() { - return new DynamicBSPSorter(this.getQuadCount()); + public boolean oldDataMatches(TranslucentGeometryCollector collector, SortType sortType, TQuad[] quads) { + // don't reuse data if we need to rewrite the mesh because of quad splitting + return !this.meshesWereModified() && super.oldDataMatches(collector, sortType, quads); + } + + @Override + public int getIndexQuadCount() { + return this.indexQuadCount; + } + + @Override + public DynamicSorter getSorter() { + // release references to the modified quad list, + // since we sort during the meshing task for the first time (in particular, when there was a non-null updated quad list) + this.updatedQuadsList = null; + + return new DynamicBSPSorter(this.getIndexQuadCount()); // index quad count + } + + @Override + public UpdatedQuadsList getUpdatedQuads() { + return this.updatedQuadsList; } - public static DynamicBSPData fromMesh(int vertexCount, - CombinedCameraPos cameraPos, TQuad[] quads, SectionPos sectionPos, - TranslucentData oldData) { + @Override + public boolean meshesWereModified() { + return this.neededQuadSplitting; + } + + public static DynamicBSPData fromMesh(CombinedCameraPos cameraPos, TQuad[] quads, SectionPos sectionPos, + TranslucentData oldData, QuadSplittingMode quadSplittingMode) { BSPNode oldRoot = null; int generation = 0; boolean prepareNodeReuse = false; + boolean allowNodeReuse = false; if (oldData instanceof DynamicBSPData oldBSPData) { generation = oldBSPData.generation + 1; oldRoot = oldBSPData.rootNode; + // disallow making use of node reuse if quad splitting ended up being needed when the tree was originally built + allowNodeReuse = !oldBSPData.neededQuadSplitting; + // only enable partial updates after a certain number of generations // (times the section has been built) - prepareNodeReuse = generation >= NODE_REUSE_MIN_GENERATION; + prepareNodeReuse = allowNodeReuse && generation >= NODE_REUSE_MIN_GENERATION; } - var result = BSPNode.buildBSP(quads, sectionPos, oldRoot, prepareNodeReuse); + var result = BSPNode.buildBSP(quads, sectionPos, oldRoot, prepareNodeReuse, allowNodeReuse, quadSplittingMode); - var dynamicData = new DynamicBSPData(sectionPos, vertexCount, result, cameraPos.getAbsoluteCameraPos(), quads, generation); + var dynamicData = new DynamicBSPData(sectionPos, quads.length, result, cameraPos.getAbsoluteCameraPos(), generation); // prepare geometry planes for integration into GFNI triggering result.prepareIntegration(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java index 6c3c69080c..bc140658d5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java @@ -5,12 +5,12 @@ import net.minecraft.core.SectionPos; import org.joml.Vector3dc; -public abstract class DynamicData extends MixedDirectionData { +public abstract class DynamicData extends PresentTranslucentData { private GeometryPlanes geometryPlanes; private final Vector3dc initialCameraPos; - DynamicData(SectionPos sectionPos, int vertexCount, int quadCount, GeometryPlanes geometryPlanes, Vector3dc initialCameraPos) { - super(sectionPos, vertexCount, quadCount); + DynamicData(SectionPos sectionPos, int inputQuadCount, GeometryPlanes geometryPlanes, Vector3dc initialCameraPos) { + super(sectionPos, inputQuadCount); this.geometryPlanes = geometryPlanes; this.initialCameraPos = initialCameraPos; } @@ -20,6 +20,8 @@ public SortType getSortType() { return SortType.DYNAMIC; } + public abstract DynamicSorter getSorter(); + public GeometryPlanes getGeometryPlanes() { return this.geometryPlanes; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java index 87539c346d..f58bdf14cb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -abstract class DynamicSorter extends Sorter { +public abstract class DynamicSorter extends PresentSorter { private final int quadCount; DynamicSorter(int quadCount) { @@ -14,4 +14,12 @@ public void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial) { this.initBufferWithQuadLength(this.quadCount); this.writeSort(cameraPos, initial); } + + public int getQuadCount() { + return this.quadCount; + } + + public int getResultSize() { + return TranslucentData.quadCountToIndexBytes(this.quadCount); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java index 200ec74a93..548d2eb51e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.GeometryPlanes; import net.caffeinemc.mods.sodium.client.util.sorting.RadixSort; import net.minecraft.core.SectionPos; @@ -9,8 +9,8 @@ import org.joml.Vector3fc; import java.nio.IntBuffer; -import java.util.Arrays; import java.util.function.IntConsumer; +import java.util.function.Supplier; /** * Performs dynamic topo sorting and falls back to distance sorting as @@ -35,32 +35,46 @@ public class DynamicTopoData extends DynamicData { private static final int PATIENT_TOPO_ATTEMPTS = 5; private static final int REGULAR_TOPO_ATTEMPTS = 2; - private boolean GFNITrigger = true; - private boolean directTrigger = false; + private boolean GFNITrigger; + private boolean directTrigger; private int consecutiveTopoSortFailures = 0; private double directTriggerKey = -1; private boolean pendingTriggerIsDirect; - private final TQuad[] quads; - private final Object2ReferenceOpenHashMap distancesByNormal; + private TQuad[] quads; + private Vector3fc[] centroids; + private Object2ReferenceMap distancesByNormal; - private DynamicTopoData(SectionPos sectionPos, int vertexCount, TQuad[] quads, + private DynamicTopoData(SectionPos sectionPos, TQuad[] quads, GeometryPlanes geometryPlanes, Vector3dc initialCameraPos, - Object2ReferenceOpenHashMap distancesByNormal) { - super(sectionPos, vertexCount, quads.length, geometryPlanes, initialCameraPos); - this.quads = quads; - this.distancesByNormal = distancesByNormal; + Supplier> distancesByNormal) { + super(sectionPos, quads.length, geometryPlanes, initialCameraPos); - if (this.getQuadCount() > MAX_TOPO_SORT_QUADS) { + if (this.getInputQuadCount() > MAX_TOPO_SORT_QUADS) { this.directTrigger = true; this.GFNITrigger = false; + + this.computeCentroids(quads); + } else { + this.directTrigger = false; + this.GFNITrigger = true; + + this.quads = quads; + this.distancesByNormal = distancesByNormal.get(); + } + } + + private void computeCentroids(TQuad[] quads) { + this.centroids = new Vector3fc[quads.length]; + for (int i = 0; i < quads.length; i++) { + this.centroids[i] = quads[i].getCenter(); } } @Override - public Sorter getSorter() { - return new DynamicTopoSorter(this.getQuadCount(), this, this.pendingTriggerIsDirect, this.consecutiveTopoSortFailures, this.GFNITrigger, this.directTrigger); + public DynamicSorter getSorter() { + return new DynamicTopoSorter(this.getInputQuadCount(), this, this.pendingTriggerIsDirect, this.consecutiveTopoSortFailures, this.GFNITrigger, this.directTrigger); } public boolean GFNITriggerEnabled() { @@ -86,6 +100,7 @@ public boolean isMatchingSorter(DynamicTopoSorter sorter) { public boolean checkAndApplyGFNITriggerOff(DynamicTopoSorter sorter) { if (this.GFNITrigger && !sorter.GFNITrigger) { this.GFNITrigger = false; + this.checkDirectSortingFallback(); return true; } return false; @@ -119,6 +134,18 @@ private void copyStateFrom(DynamicTopoSorter sorter) { this.GFNITrigger = sorter.GFNITrigger; this.directTrigger = sorter.directTrigger; this.consecutiveTopoSortFailures = sorter.consecutiveTopoSortFailuresNew; + + this.checkDirectSortingFallback(); + } + + private void checkDirectSortingFallback() { + // once the GFNI trigger is turned off, the topo sort data is never used again, so it can be freed to save memory. + if (!this.GFNITrigger && this.quads != null) { + this.computeCentroids(this.quads); + + this.quads = null; + this.distancesByNormal = null; + } } @Override @@ -172,7 +199,7 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { if (this.GFNITrigger && !this.isDirectTrigger) { this.intBuffer = indexBuffer; var sortStart = initial ? 0 : System.nanoTime(); - var result = TopoGraphSorting.topoGraphSort(this, DynamicTopoData.this.quads, DynamicTopoData.this.distancesByNormal, cameraPos.getRelativeCameraPos()); + var result = TopoGraphSorting.topoGraphSort(this, DynamicTopoData.this.quads, DynamicTopoData.this.distancesByNormal, cameraPos.getRelativeCameraPos(), false); this.intBuffer = null; var sortTime = initial ? 0 : System.nanoTime() - sortStart; @@ -206,7 +233,7 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { if (this.directTrigger) { indexBuffer.rewind(); - distanceSortDirect(indexBuffer, DynamicTopoData.this.quads, cameraPos.getRelativeCameraPos()); + distanceSortDirect(indexBuffer, DynamicTopoData.this.centroids, DynamicTopoData.this.quads, cameraPos.getRelativeCameraPos()); } if (initial) { @@ -219,42 +246,41 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { * Sorts the given quads by descending center distance to the camera and writes * the resulting order to the given index buffer. */ - static void distanceSortDirect(IntBuffer indexBuffer, TQuad[] quads, Vector3fc cameraPos) { - if (quads.length <= 1) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, 0); - } else if (RadixSort.useRadixSort(quads.length)) { - final var keys = new int[quads.length]; - - for (int q = 0; q < quads.length; q++) { - keys[q] = ~Float.floatToRawIntBits(quads[q].getCenter().distanceSquared(cameraPos)); - } - - var indices = RadixSort.sort(keys); + static void distanceSortDirect(IntBuffer indexBuffer, Vector3fc[] centroids, TQuad[] quads, Vector3fc cameraPos) { + int count; + if (centroids != null) { + count = centroids.length; + } else { + count = quads.length; + } - for (int i = 0; i < quads.length; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, indices[i]); - } + if (count <= 1) { + // Avoid allocations when there is nothing to sort. + TranslucentData.writeQuadVertexIndexes(indexBuffer, 0); } else { - final var data = new long[quads.length]; - for (int q = 0; q < quads.length; q++) { - float distance = quads[q].getCenter().distanceSquared(cameraPos); - data[q] = (long) ~Float.floatToRawIntBits(distance) << 32 | q; + final var keys = new int[count]; + final var perm = new int[count]; + + for (int idx = 0; idx < count; idx++) { + Vector3fc centroid; + if (centroids != null) { + centroid = centroids[idx]; + } else { + centroid = quads[idx].getCenter(); + } + keys[idx] = ~Float.floatToRawIntBits(centroid.distanceSquared(cameraPos)); + perm[idx] = idx; } - Arrays.sort(data); + RadixSort.sortIndirect(perm, keys, false); - for (int i = 0; i < quads.length; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, (int) data[i]); + for (int idx = 0; idx < count; idx++) { + TranslucentData.writeQuadVertexIndexes(indexBuffer, perm[idx]); } } } - public static DynamicTopoData fromMesh(int vertexCount, - CombinedCameraPos cameraPos, TQuad[] quads, SectionPos sectionPos, - GeometryPlanes geometryPlanes) { - var distancesByNormal = geometryPlanes.prepareAndGetDistances(); - - return new DynamicTopoData(sectionPos, vertexCount, quads, geometryPlanes, - cameraPos.getAbsoluteCameraPos(), distancesByNormal); + public static DynamicTopoData fromMesh(CombinedCameraPos cameraPos, TQuad[] quads, SectionPos sectionPos, GeometryPlanes geometryPlanes) { + return new DynamicTopoData(sectionPos, quads, geometryPlanes, cameraPos.getAbsoluteCameraPos(), geometryPlanes::prepareAndGetDistances); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/MixedDirectionData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/MixedDirectionData.java deleted file mode 100644 index a7ab11d965..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/MixedDirectionData.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; - -import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; -import net.minecraft.core.SectionPos; - -public abstract class MixedDirectionData extends PresentTranslucentData { - private final int[] vertexCounts = new int[ModelQuadFacing.COUNT]; - - MixedDirectionData(SectionPos sectionPos, int vertexCount, int quadCount) { - super(sectionPos, quadCount); - this.vertexCounts[ModelQuadFacing.UNASSIGNED.ordinal()] = vertexCount; - } - - @Override - public int[] getVertexCounts() { - return this.vertexCounts; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/NoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/NoData.java index a6f281479c..c99c9e9ab9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/NoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/NoData.java @@ -1,12 +1,14 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; /** * This class means there is no translucent data and is used to signal that the * section should be removed from triggering data structures. - * + *

* If translucent sorting is disabled, not even this class is used, but null is * passed instead. */ @@ -23,6 +25,11 @@ public SortType getSortType() { return this.reason; } + @Override + public boolean oldDataMatches(TranslucentGeometryCollector collector, SortType sortType, TQuad[] quads) { + return false; + } + public static NoData forEmptySection(SectionPos sectionPos) { return new NoData(sectionPos, SortType.EMPTY_SECTION); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentSorter.java new file mode 100644 index 0000000000..81358a07a4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentSorter.java @@ -0,0 +1,23 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; + +import net.caffeinemc.mods.sodium.client.util.NativeBuffer; + +public abstract class PresentSorter implements Sorter { + private NativeBuffer indexBuffer; + + @Override + public NativeBuffer getIndexBuffer() { + return this.indexBuffer; + } + + void initBufferWithQuadLength(int quadCount) { + this.indexBuffer = new NativeBuffer(TranslucentData.quadCountToIndexBytes(quadCount)); + } + + @Override + public void destroy() { + if (this.indexBuffer != null) { + this.indexBuffer.free(); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentTranslucentData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentTranslucentData.java index d4e219e3a7..220af9f7b3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentTranslucentData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/PresentTranslucentData.java @@ -1,32 +1,43 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; /** * Super class for translucent data that contains an actual buffer. */ public abstract class PresentTranslucentData extends TranslucentData { - protected final int quadCount; + private final int inputQuadCount; private int quadHash; - PresentTranslucentData(SectionPos sectionPos, int quadCount) { + PresentTranslucentData(SectionPos sectionPos, int inputQuadCount) { super(sectionPos); - this.quadCount = quadCount; + this.inputQuadCount = inputQuadCount; } - public abstract int[] getVertexCounts(); - public abstract Sorter getSorter(); + @Override + public boolean oldDataMatches(TranslucentGeometryCollector collector, SortType sortType, TQuad[] quads) { + // for the sort types other than NONE (and the old data being AnyOrderData) the geometry needs to be the same (checked with length and hash) + return this.getInputQuadCount() == quads.length && this.hashMatches(collector); + } + + protected boolean hashMatches(TranslucentGeometryCollector collector) { + return this.quadHash == collector.getQuadHash(); + } + public void setQuadHash(int hash) { this.quadHash = hash; } - public int getQuadHash() { - return this.quadHash; + public int getInputQuadCount() { + return this.inputQuadCount; } - public int getQuadCount() { - return this.quadCount; + public int getIndexQuadCount() { + return this.inputQuadCount; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SharedIndexSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SharedIndexSorter.java new file mode 100644 index 0000000000..9dc709a761 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SharedIndexSorter.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; + +import net.caffeinemc.mods.sodium.client.util.NativeBuffer; + +import java.nio.IntBuffer; + +public record SharedIndexSorter(int quadCount) implements Sorter { + @Override + public NativeBuffer getIndexBuffer() { + return null; + } + + @Override + public IntBuffer getIntBuffer() { + return null; + } + + @Override + public void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial) { + // no-op + } + + @Override + public void destroy() { + // no-op + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SortData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SortData.java deleted file mode 100644 index c543a82335..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SortData.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; - -public interface SortData extends PresentSortData { - boolean isReusingUploadedIndexData(); -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/Sorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/Sorter.java index 1103773793..545e58a2b9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/Sorter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/Sorter.java @@ -1,18 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import net.caffeinemc.mods.sodium.client.util.NativeBuffer; +public interface Sorter extends PresentSortData { + void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial); -public abstract class Sorter implements PresentSortData { - private NativeBuffer indexBuffer; - - public abstract void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial); - - @Override - public NativeBuffer getIndexBuffer() { - return this.indexBuffer; - } - - void initBufferWithQuadLength(int quadCount) { - this.indexBuffer = new NativeBuffer(TranslucentData.quadCountToIndexBytes(quadCount)); - } + void destroy(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SplitDirectionData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SplitDirectionData.java deleted file mode 100644 index cc07ffe669..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/SplitDirectionData.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; - -import net.minecraft.core.SectionPos; - -/** - * Super class for translucent data that is rendered separately for each facing. - * (block face culling is possible) It's important that the indices are inserted - * starting at zero for each facing. - */ -public abstract class SplitDirectionData extends PresentTranslucentData { - private final int[] vertexCounts; - - public SplitDirectionData(SectionPos sectionPos, int[] vertexCounts, int quadCount) { - super(sectionPos, quadCount); - this.vertexCounts = vertexCounts; - } - - @Override - public int[] getVertexCounts() { - return this.vertexCounts; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java index 888ccd2034..a77e8f033f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java @@ -1,25 +1,23 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.sorting.RadixSort; import net.minecraft.core.SectionPos; -import java.util.Arrays; - /** * Static normal relative sorting orders quads by the dot product of their * normal and position. (referred to as "distance" throughout the code) - * + *

* Unlike sorting by distance, which is descending for translucent rendering to * be correct, sorting by dot product is ascending instead. */ -public class StaticNormalRelativeData extends SplitDirectionData { +public class StaticNormalRelativeData extends PresentTranslucentData { private Sorter sorterOnce; - public StaticNormalRelativeData(SectionPos sectionPos, int[] vertexCounts, int quadCount) { - super(sectionPos, vertexCounts, quadCount); + public StaticNormalRelativeData(SectionPos sectionPos, int inputQuadCount) { + super(sectionPos, inputQuadCount); } @Override @@ -37,38 +35,28 @@ public Sorter getSorter() { return sorter; } - private static StaticNormalRelativeData fromDoubleUnaligned(int[] vertexCounts, TQuad[] quads, SectionPos sectionPos) { - var snrData = new StaticNormalRelativeData(sectionPos, vertexCounts, quads.length); + private static StaticNormalRelativeData fromDoubleUnaligned(TQuad[] quads, SectionPos sectionPos) { + var snrData = new StaticNormalRelativeData(sectionPos, quads.length); var sorter = new StaticSorter(quads.length); snrData.sorterOnce = sorter; var indexBuffer = sorter.getIntBuffer(); if (quads.length <= 1) { + // Avoid allocations when there is nothing to sort. TranslucentData.writeQuadVertexIndexes(indexBuffer, 0); - } else if (RadixSort.useRadixSort(quads.length)) { + } else { final var keys = new int[quads.length]; + final var perm = new int[quads.length]; for (int q = 0; q < quads.length; q++) { keys[q] = MathUtil.floatToComparableInt(quads[q].getAccurateDotProduct()); + perm[q] = q; } - var indices = RadixSort.sort(keys); + RadixSort.sortIndirect(perm, keys, false); for (int i = 0; i < quads.length; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, indices[i]); - } - } else { - final var sortData = new long[quads.length]; - - for (int q = 0; q < quads.length; q++) { - int dotProductComponent = MathUtil.floatToComparableInt(quads[q].getAccurateDotProduct()); - sortData[q] = (long) dotProductComponent << 32 | q; - } - - Arrays.sort(sortData); - - for (int i = 0; i < quads.length; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, (int) sortData[i]); + TranslucentData.writeQuadVertexIndexes(indexBuffer, perm[i]); } } @@ -78,64 +66,46 @@ private static StaticNormalRelativeData fromDoubleUnaligned(int[] vertexCounts, /** * Important: The vertex indexes must start at zero for each facing. */ - private static StaticNormalRelativeData fromMixed(int[] vertexCounts, + private static StaticNormalRelativeData fromMixed(int[] meshFacingCounts, TQuad[] quads, SectionPos sectionPos) { - var snrData = new StaticNormalRelativeData(sectionPos, vertexCounts, quads.length); + var snrData = new StaticNormalRelativeData(sectionPos, quads.length); var sorter = new StaticSorter(quads.length); snrData.sorterOnce = sorter; var indexBuffer = sorter.getIntBuffer(); var maxQuadCount = 0; - boolean anyNeedsSortData = false; - for (var vertexCount : vertexCounts) { - if (vertexCount != -1) { - var quadCount = TranslucentData.vertexCountToQuadCount(vertexCount); + + for (var quadCount : meshFacingCounts) { + if (quadCount != -1) { maxQuadCount = Math.max(maxQuadCount, quadCount); - anyNeedsSortData |= !RadixSort.useRadixSort(quadCount) && quadCount > 1; } } - long[] sortData = null; - if (anyNeedsSortData) { - sortData = new long[maxQuadCount]; - } - + // The quad index is used to keep track of the position in the quad array. + // This is necessary because the emitted quad indexes in each facing start at zero, + // but the quads are stored in a single continuously indexed array. int quadIndex = 0; - for (var vertexCount : vertexCounts) { - if (vertexCount == -1 || vertexCount == 0) { + for (var quadCount : meshFacingCounts) { + if (quadCount == -1 || quadCount == 0) { continue; } - int count = TranslucentData.vertexCountToQuadCount(vertexCount); - - if (count == 1) { + if (quadCount == 1) { TranslucentData.writeQuadVertexIndexes(indexBuffer, 0); quadIndex++; - } else if (RadixSort.useRadixSort(count)) { - final var keys = new int[count]; - - for (int q = 0; q < count; q++) { - keys[q] = MathUtil.floatToComparableInt(quads[quadIndex++].getAccurateDotProduct()); - } - - var indices = RadixSort.sort(keys); - - for (int i = 0; i < count; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, indices[i]); - } } else { - for (int i = 0; i < count; i++) { - var quad = quads[quadIndex++]; - int dotProductComponent = MathUtil.floatToComparableInt(quad.getAccurateDotProduct()); - sortData[i] = (long) dotProductComponent << 32 | i; - } + final var keys = new int[quadCount]; + final var perm = new int[quadCount]; - if (count > 1) { - Arrays.sort(sortData, 0, count); + for (int idx = 0; idx < quadCount; idx++) { + keys[idx] = MathUtil.floatToComparableInt(quads[quadIndex++].getAccurateDotProduct()); + perm[idx] = idx; } - for (int i = 0; i < count; i++) { - TranslucentData.writeQuadVertexIndexes(indexBuffer, (int) sortData[i]); + RadixSort.sortIndirect(perm, keys, false); + + for (int idx = 0; idx < quadCount; idx++) { + TranslucentData.writeQuadVertexIndexes(indexBuffer, perm[idx]); } } } @@ -143,12 +113,12 @@ private static StaticNormalRelativeData fromMixed(int[] vertexCounts, return snrData; } - public static StaticNormalRelativeData fromMesh(int[] vertexCounts, + public static StaticNormalRelativeData fromMesh(int[] meshFacingCounts, TQuad[] quads, SectionPos sectionPos, boolean isDoubleUnaligned) { if (isDoubleUnaligned) { - return fromDoubleUnaligned(vertexCounts, quads, sectionPos); + return fromDoubleUnaligned(quads, sectionPos); } else { - return fromMixed(vertexCounts, quads, sectionPos); + return fromMixed(meshFacingCounts, quads, sectionPos); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticSorter.java index fe6bc4d133..cbc09e19ed 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticSorter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticSorter.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -class StaticSorter extends Sorter { +class StaticSorter extends PresentSorter { StaticSorter(int quadCount) { this.initBufferWithQuadLength(quadCount); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticTopoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticTopoData.java index 48d6bb298b..6b9324d236 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticTopoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticTopoData.java @@ -1,8 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; import java.nio.IntBuffer; @@ -13,11 +12,11 @@ * possible to sort without dynamic triggering, meaning the sort order never * needs to change. */ -public class StaticTopoData extends MixedDirectionData { +public class StaticTopoData extends PresentTranslucentData { private Sorter sorterOnce; - StaticTopoData(SectionPos sectionPos, int vertexCount, int quadCount) { - super(sectionPos, vertexCount, quadCount); + StaticTopoData(SectionPos sectionPos, int inputQuadCount) { + super(sectionPos, inputQuadCount); } @Override @@ -42,16 +41,16 @@ public void accept(int value) { } } - public static StaticTopoData fromMesh(int vertexCount, TQuad[] quads, SectionPos sectionPos) { + public static StaticTopoData fromMesh(TQuad[] quads, SectionPos sectionPos, boolean failOnIntersection) { var sorter = new StaticSorter(quads.length); var indexWriter = new QuadIndexConsumerIntoBuffer(sorter.getIntBuffer()); - if (!TopoGraphSorting.topoGraphSort(indexWriter, quads, null, null)) { + if (!TopoGraphSorting.topoGraphSort(indexWriter, quads, null, null, failOnIntersection)) { sorter.getIndexBuffer().free(); return null; } - var staticTopoData = new StaticTopoData(sectionPos, vertexCount, quads.length); + var staticTopoData = new StaticTopoData(sectionPos, quads.length); staticTopoData.sorterOnce = sorter; return staticTopoData; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java index eb8e9b3dea..d3792db7a1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java @@ -1,9 +1,9 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.AlignableNormal; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.NormalList; import net.caffeinemc.mods.sodium.client.util.collections.BitArray; import org.joml.Vector3fc; @@ -69,7 +69,7 @@ private static boolean pointOutsideHalfSpaceEpsilon(float planeDistance, Vector3 return planeNormal.dot(x, y, z) - HALF_SPACE_EPSILON > planeDistance; } - public static boolean orthogonalQuadVisibleThrough(TQuad quadA, TQuad quadB) { + public static boolean orthogonalQuadVisibleThrough(TQuad quadA, TQuad quadB, boolean intersectionsVisible) { var aDirection = quadA.getFacing().ordinal(); var aOpposite = quadA.getFacing().getOpposite().ordinal(); var bDirection = quadB.getFacing().ordinal(); @@ -85,24 +85,27 @@ public static boolean orthogonalQuadVisibleThrough(TQuad quadA, TQuad quadB) { var vis = BIntoADescent > 0 && AOutsideBAscent > 0; - // if they're visible and their bounding boxes intersect and apply a heuristic to resolve + // if they're visible and their bounding boxes intersect, apply a heuristic to resolve if allowed if (vis && TQuad.extentsIntersect(aExtents, bExtents)) { + if (intersectionsVisible) { + return true; + } return BIntoADescent + AOutsideBAscent > 1; } return vis; } - private static boolean testSeparatorRange(Object2ReferenceOpenHashMap distancesByNormal, + private static boolean testSeparatorRange(Object2ReferenceMap distancesByNormal, Vector3fc normal, float start, float end) { var distances = distancesByNormal.get(normal); if (distances == null) { return false; } - return AlignableNormal.queryRange(distances, start, end); + return NormalList.queryRange(distances, start, end); } private static boolean visibilityWithSeparator(TQuad quadA, TQuad quadB, - Object2ReferenceOpenHashMap distancesByNormal, Vector3fc cameraPos) { + Object2ReferenceMap distancesByNormal, Vector3fc cameraPos) { // check if there is an aligned separator for (int direction = 0; direction < ModelQuadFacing.DIRECTIONS; direction++) { var facing = ModelQuadFacing.VALUES[direction]; @@ -157,13 +160,15 @@ private static boolean visibilityWithSeparator(TQuad quadA, TQuad quadB, * Checks if one quad is visible through the other quad. This accepts arbitrary * quads, even unaligned ones. * - * @param quad the quad through which the other quad is being tested - * @param other the quad being tested - * @param distancesByNormal a map of normals to sorted arrays of face plane distances for disproving that the quads are visible through each other, null to disable + * @param quad the quad through which the other quad is being tested + * @param other the quad being tested + * @param distancesByNormal a map of normals to sorted arrays of face plane distances for disproving that the quads are visible through each other, null to disable + * @param cameraPos the camera position, or null to disable the visibility check + * @param intersectionsVisible if true, the method will return true if the quads intersect instead of trying to break the tie * @return true if the other quad is visible through the first quad */ private static boolean quadVisibleThrough(TQuad quad, TQuad other, - Object2ReferenceOpenHashMap distancesByNormal, Vector3fc cameraPos) { + Object2ReferenceMap distancesByNormal, Vector3fc cameraPos, boolean intersectionsVisible) { if (quad == other) { return false; } @@ -186,7 +191,7 @@ private static boolean quadVisibleThrough(TQuad quad, TQuad other, result = sign * quad.getExtents()[direction] > sign * other.getExtents()[direction]; } else { // orthogonal quads - result = orthogonalQuadVisibleThrough(quad, other); + result = orthogonalQuadVisibleThrough(quad, other, intersectionsVisible); } } else { // at least one unaligned quad @@ -241,17 +246,18 @@ private static boolean quadVisibleThrough(TQuad quad, TQuad other, * doing a DFS on the implicit graph. Edges are tested as they are searched for * and if necessary separator planes are used to disprove visibility. * - * @param indexConsumer the consumer to write the topo sort result to - * @param allQuads the quads to sort - * @param distancesByNormal a map of normals to sorted arrays of face plane - * distances, null to disable - * @param cameraPos the camera position, or null to disable the - * visibility check + * @param indexConsumer the consumer to write the topo sort result to + * @param allQuads the quads to sort + * @param distancesByNormal a map of normals to sorted arrays of face plane + * distances, null to disable + * @param cameraPos the camera position, or null to disable the + * visibility check + * @param failOnIntersection if true, intersecting orthogonal quads are treated as visible to each other */ public static boolean topoGraphSort( IntConsumer indexConsumer, TQuad[] allQuads, - Object2ReferenceOpenHashMap distancesByNormal, - Vector3fc cameraPos) { + Object2ReferenceMap distancesByNormal, + Vector3fc cameraPos, boolean failOnIntersection) { // if enabled, check for visibility and produce a mapping of indices TQuad[] quads; int[] activeToRealIndex = null; @@ -281,10 +287,10 @@ public static boolean topoGraphSort( quadCount = allQuads.length; } - return topoGraphSort(indexConsumer, quads, quadCount, activeToRealIndex, distancesByNormal, cameraPos); + return topoGraphSort(indexConsumer, quads, quadCount, activeToRealIndex, distancesByNormal, cameraPos, failOnIntersection); } - public static boolean topoGraphSort(IntConsumer indexConsumer, TQuad[] quads, int quadCount, int[] activeToRealIndex, Object2ReferenceOpenHashMap distancesByNormal, Vector3fc cameraPos) { + public static boolean topoGraphSort(IntConsumer indexConsumer, TQuad[] quads, int quadCount, int[] activeToRealIndex, Object2ReferenceMap distancesByNormal, Vector3fc cameraPos, boolean failOnIntersection) { // special case for 0 to 2 quads if (quadCount == 0) { return true; @@ -302,7 +308,12 @@ public static boolean topoGraphSort(IntConsumer indexConsumer, TQuad[] quads, in if (quadCount == 2) { var a = 0; var b = 1; - if (quadVisibleThrough(quads[a], quads[b], null, null)) { + if (quadVisibleThrough(quads[a], quads[b], null, null, failOnIntersection)) { + // fail on cycle if required by flag + if (failOnIntersection && quadVisibleThrough(quads[b], quads[a], null, null, true)) { + return false; + } + a = 1; b = 0; } @@ -339,7 +350,7 @@ public static boolean topoGraphSort(IntConsumer indexConsumer, TQuad[] quads, in if (currentQuadIndex != nextEdgeTest) { var currentQuad = quads[currentQuadIndex]; var nextQuad = quads[nextEdgeTest]; - if (quadVisibleThrough(currentQuad, nextQuad, distancesByNormal, cameraPos)) { + if (quadVisibleThrough(currentQuad, nextQuad, distancesByNormal, cameraPos, failOnIntersection)) { // if the visible quad is on the stack, there is a cycle if (onStack.getAndSet(nextEdgeTest)) { return false; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TranslucentData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TranslucentData.java index 4a683bc09c..635201bd0f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TranslucentData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TranslucentData.java @@ -1,11 +1,13 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import java.nio.IntBuffer; - -import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.bsp_tree.UpdatedQuadsList; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; +import java.nio.IntBuffer; + /** * The base class for all types of translucent data. Subclasses are generated by * the geometry collector after the section is built. @@ -24,10 +26,20 @@ public abstract class TranslucentData { public abstract SortType getSortType(); + public abstract boolean oldDataMatches(TranslucentGeometryCollector collector, SortType sortType, TQuad[] quads); + + public UpdatedQuadsList getUpdatedQuads() { + return null; + } + + public boolean meshesWereModified() { + return false; + } + /** * Prepares the translucent data for triggering of the given type. This is run * on the main thread before a sort task is scheduled. - * + * * @param isAngleTrigger Whether the trigger is an angle trigger */ public void prepareTrigger(boolean isAngleTrigger) { @@ -42,8 +54,8 @@ public static int quadCountToIndexBytes(int quadCount) { return quadCount * BYTES_PER_QUAD; } - public static int indexBytesToQuadCount(int indexBytes) { - return indexBytes / BYTES_PER_QUAD; + public static int quadCountToVertexCount(int quadCount) { + return quadCount * VERTICES_PER_QUAD; } public static void writeQuadVertexIndexes(IntBuffer intBuffer, int quadIndex) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/FullTQuad.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/FullTQuad.java new file mode 100644 index 0000000000..4bc1ed2235 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/FullTQuad.java @@ -0,0 +1,178 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad; + +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.DefaultMaterials; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; +import org.joml.Vector3f; +import org.joml.Vector3fc; + +import java.nio.ByteBuffer; + +public class FullTQuad extends RegularTQuad { + private final ChunkVertexEncoder.Vertex[] vertices = ChunkVertexEncoder.Vertex.uninitializedQuad(); + private int sameVertexMap; + private boolean normalIsVeryAccurate = false; + + private boolean hasUpdatedVertices = false; + + // NO_WRITE means it should not be written (either there is no update or it's getting overwritten) + private static final int NO_WRITE = -1; + private int writeToIndex = NO_WRITE; + + FullTQuad(ModelQuadFacing facing, int packedNormal) { + super(facing, packedNormal); + } + + public static FullTQuad fromVertices(ChunkVertexEncoder.Vertex[] vertices, ModelQuadFacing facing, int packedNormal) { + var quad = new FullTQuad(facing, packedNormal); + quad.sameVertexMap = quad.initExtentsAndCenter(vertices); + if (quad.isInvalid()) { + return null; + } + + quad.initDotProduct(); + quad.initVertices(vertices); + + return quad; + } + + private void initVertices(ChunkVertexEncoder.Vertex[] vertices) { + // deep copy the vertices since the caller may modify them + for (int i = 0; i < 4; i++) { + var newVertex = this.vertices[i]; + var oldVertex = vertices[i]; + ChunkVertexEncoder.Vertex.copyVertexTo(oldVertex, newVertex); + } + } + + public static FullTQuad splittingCopy(FullTQuad quad) { + var newQuad = new FullTQuad(quad.facing, quad.packedNormal); + newQuad.initVertices(quad.vertices); + + newQuad.extents = quad.extents; + newQuad.accurateDotProduct = quad.accurateDotProduct; + newQuad.quantizedDotProduct = quad.quantizedDotProduct; + + newQuad.center = quad.center; + newQuad.quantizedNormal = quad.quantizedNormal; + newQuad.accurateNormal = quad.accurateNormal; + + newQuad.normalIsVeryAccurate = quad.normalIsVeryAccurate; + + return newQuad; + } + + public void updateSplitQuadAfterVertexModification() { + this.sameVertexMap = this.initExtentsAndCenter(this.vertices); + + // invalidate vertex positions after modification of the vertices + this.vertexPositions = null; + + // no need to update dot product since splitting a quad doesn't change its normal or dot product + } + + public boolean isInvalid() { + return isInvalid(this.sameVertexMap); + } + + public int getUniqueVertexMap() { + return (~this.sameVertexMap) & 0b1111; + } + + public int getSameVertexMap() { + return this.sameVertexMap; + } + + public boolean triggerAndSetUpdatedVertices() { + if (this.hasUpdatedVertices) { + return false; + } + + this.hasUpdatedVertices = true; + return true; + } + + public void setWriteToIndex(int writeToIndex) { + this.writeToIndex = writeToIndex; + } + + public void setNoWrite() { + this.writeToIndex = NO_WRITE; + } + + public void writeToBuffer(ChunkMeshBufferBuilder bufferBuilder, ByteBuffer buffer) { + if (this.writeToIndex != NO_WRITE) { + bufferBuilder.writeExternal(buffer, TranslucentData.quadCountToVertexCount(this.writeToIndex), this.vertices, DefaultMaterials.TRANSLUCENT); + } + } + + @Override + public float[] getVertexPositions() { + if (this.vertexPositions == null) { + this.vertexPositions = new float[12]; + + for (int i = 0; i < 4; i++) { + this.vertexPositions[i * 3] = this.vertices[i].x; + this.vertexPositions[i * 3 + 1] = this.vertices[i].y; + this.vertexPositions[i * 3 + 2] = this.vertices[i].z; + } + } + + return this.vertexPositions; + } + + public Vector3fc getVeryAccurateNormal() { + if (this.facing.isAligned()) { + return this.facing.getAlignedNormal(); + } else { + if (!this.normalIsVeryAccurate) { + final float x0 = this.vertices[0].x; + final float y0 = this.vertices[0].y; + final float z0 = this.vertices[0].z; + + final float x1 = this.vertices[1].x; + final float y1 = this.vertices[1].y; + final float z1 = this.vertices[1].z; + + final float x2 = this.vertices[2].x; + final float y2 = this.vertices[2].y; + final float z2 = this.vertices[2].z; + + final float x3 = this.vertices[3].x; + final float y3 = this.vertices[3].y; + final float z3 = this.vertices[3].z; + + 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; + + // normalize by length for the packed normal + // TODO: normalization necessary? + float length = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ); + if (length != 0.0 && length != 1.0) { + normX /= length; + normY /= length; + normZ /= length; + } + + this.accurateNormal = new Vector3f(normX, normY, normZ); + this.accurateDotProduct = this.accurateNormal.dot(this.center); + this.normalIsVeryAccurate = true; + } + } + return this.accurateNormal; + } + + public ChunkVertexEncoder.Vertex[] getVertices() { + return this.vertices; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/RegularTQuad.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/RegularTQuad.java new file mode 100644 index 0000000000..032d43bc5f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/RegularTQuad.java @@ -0,0 +1,85 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad; + +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; + +public class RegularTQuad extends TQuad { + float[] vertexPositions; + + RegularTQuad(ModelQuadFacing facing, int packedNormal) { + super(facing, packedNormal); + } + + public static RegularTQuad fromVertices(ChunkVertexEncoder.Vertex[] vertices, ModelQuadFacing facing, int packedNormal) { + var quad = new RegularTQuad(facing, packedNormal); + + var sameVertexMap = quad.initExtentsAndCenter(vertices); + if (isInvalid(sameVertexMap)) { + return null; + } + + quad.initVertexPositions(vertices, sameVertexMap); + quad.initDotProduct(); + + return quad; + } + + void initVertexPositions(ChunkVertexEncoder.Vertex[] vertices, int sameVertexMap) { + // check if we need to store vertex positions for this quad, only necessary if it's unaligned or rotated (yet aligned) + var needsVertexPositions = (sameVertexMap != 0 || !this.facing.isAligned()); + if (!needsVertexPositions) { + float posXExtent = this.extents[0]; + float posYExtent = this.extents[1]; + float posZExtent = this.extents[2]; + float negXExtent = this.extents[3]; + float negYExtent = this.extents[4]; + float negZExtent = this.extents[5]; + + for (int i = 0; i < 4; i++) { + var vertex = vertices[i]; + if (vertex.x != posYExtent && vertex.x != negYExtent || + vertex.y != posZExtent && vertex.y != negZExtent || + vertex.z != posXExtent && vertex.z != negXExtent) { + needsVertexPositions = true; + break; + } + } + } + + if (needsVertexPositions) { + var vertexPositions = new float[12]; + this.vertexPositions = vertexPositions; + for (int i = 0, itemIndex = 0; i < 4; i++) { + var vertex = vertices[i]; + vertexPositions[itemIndex++] = vertex.x; + vertexPositions[itemIndex++] = vertex.y; + vertexPositions[itemIndex++] = vertex.z; + } + } + } + + public float[] getVertexPositions() { + // calculate vertex positions from extents if there's no cached value + // (we don't want to be preemptively collecting vertex positions for all aligned quads) + if (this.vertexPositions == null) { + this.vertexPositions = new float[12]; + + var facingAxis = this.facing.getAxis(); + var xRange = facingAxis == 0 ? 0 : 3; + var yRange = facingAxis == 1 ? 0 : 3; + var zRange = facingAxis == 2 ? 0 : 3; + + var itemIndex = 0; + for (int x = 0; x <= xRange; x += 3) { + for (int y = 0; y <= yRange; y += 3) { + for (int z = 0; z <= zRange; z += 3) { + this.vertexPositions[itemIndex++] = this.extents[x]; + this.vertexPositions[itemIndex++] = this.extents[y + 1]; + this.vertexPositions[itemIndex++] = this.extents[z + 2]; + } + } + } + } + return this.vertexPositions; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/TQuad.java similarity index 55% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/TQuad.java index 8d0ae60c73..cfdd7eee94 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/quad/TQuad.java @@ -1,18 +1,27 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; - -import java.util.Arrays; +package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad; +import net.caffeinemc.mods.sodium.api.util.NormI8; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.DefaultFluidRenderer; +import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; import org.joml.Vector3f; import org.joml.Vector3fc; -import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; -import net.caffeinemc.mods.sodium.api.util.NormI8; +import java.util.Arrays; /** * Represents a quad for the purposes of translucency sorting. Called TQuad to * avoid confusion with other quad classes. */ -public class TQuad { +public abstract class TQuad { + /** + * If the delta between two vertices is smaller than this value, they are + * considered to be the same vertex. This is also used for checking whether a vertex + * lies on a splitting plane and whether the result of a splitting operation results + * in an empty quad. + */ + public static final float VERTEX_EPSILON = 0.00001f; + /** * The quantization factor with which the normals are quantized such that there * are fewer possible unique normals. The factor describes the number of steps @@ -21,25 +30,129 @@ public class TQuad { * at the origin onto which the normals are projected. The normals are snapped * to the nearest grid point. */ - private static final int QUANTIZATION_FACTOR = 4; - - private ModelQuadFacing facing; - private final float[] extents; - private float[] vertexPositions; - private final int packedNormal; - private final float accurateDotProduct; - private float quantizedDotProduct; - private Vector3fc center; // null on aligned quads - private Vector3fc quantizedNormal; - private Vector3fc accurateNormal; - - private TQuad(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center, int packedNormal) { + static final int NORMAL_QUANTIZATION_STEPS = 4; + + private static final float INV_QUANTIZE_EPSILON = 256f; + public static final float QUANTIZE_EPSILON = 1f / INV_QUANTIZE_EPSILON; + + static { + // ensure it fits with the fluid renderer epsilon and that it's a power-of-two + // fraction + var targetEpsilon = DefaultFluidRenderer.EPSILON * 2.1f; + if (QUANTIZE_EPSILON <= targetEpsilon && Integer.bitCount((int) INV_QUANTIZE_EPSILON) == 1) { + throw new RuntimeException("epsilon is invalid: " + QUANTIZE_EPSILON); + } + } + + ModelQuadFacing facing; + final int packedNormal; + float[] extents; + float accurateDotProduct; + float quantizedDotProduct; + Vector3fc center; // null on aligned quads + Vector3fc quantizedNormal; + Vector3fc accurateNormal; + + TQuad(ModelQuadFacing facing, int packedNormal) { + if (facing.isAligned()) { + packedNormal = ModelQuadFacing.PACKED_ALIGNED_NORMALS[facing.ordinal()]; + } + this.facing = facing; - this.extents = extents; - this.vertexPositions = vertexPositions; - this.center = center; this.packedNormal = packedNormal; + } + + protected static boolean isInvalid(int sameVertexMap) { + return Integer.bitCount(sameVertexMap) > 1; + } + + int initExtentsAndCenter(ChunkVertexEncoder.Vertex[] vertices) { + float xSum = 0; + float ySum = 0; + float zSum = 0; + + // keep track of distinct vertices to compute the center accurately for + // degenerate quads + float lastX = vertices[3].x; + float lastY = vertices[3].y; + float lastZ = vertices[3].z; + int sameVertexMap = 0; + + float posXExtent = Float.NEGATIVE_INFINITY; + float posYExtent = Float.NEGATIVE_INFINITY; + float posZExtent = Float.NEGATIVE_INFINITY; + float negXExtent = Float.POSITIVE_INFINITY; + float negYExtent = Float.POSITIVE_INFINITY; + float negZExtent = Float.POSITIVE_INFINITY; + + for (int i = 0; i < 4; i++) { + float x = vertices[i].x; + float y = vertices[i].y; + float z = vertices[i].z; + + posXExtent = Math.max(posXExtent, x); + posYExtent = Math.max(posYExtent, y); + posZExtent = Math.max(posZExtent, z); + negXExtent = Math.min(negXExtent, x); + negYExtent = Math.min(negYExtent, y); + negZExtent = Math.min(negZExtent, z); + + if (Math.abs(x - lastX) >= VERTEX_EPSILON || + Math.abs(y - lastY) >= VERTEX_EPSILON || + Math.abs(z - lastZ) >= VERTEX_EPSILON) { + xSum += x; + ySum += y; + zSum += z; + } else { + sameVertexMap |= 1 << i; + } + if (i != 3) { + lastX = x; + lastY = y; + lastZ = z; + } + } + + // shrink quad in non-normal directions to prevent intersections caused by + // epsilon offsets applied by FluidRenderer + if (this.facing != ModelQuadFacing.POS_X && this.facing != ModelQuadFacing.NEG_X) { + posXExtent -= QUANTIZE_EPSILON; + negXExtent += QUANTIZE_EPSILON; + if (negXExtent > posXExtent) { + negXExtent = posXExtent; + } + } + if (this.facing != ModelQuadFacing.POS_Y && this.facing != ModelQuadFacing.NEG_Y) { + posYExtent -= QUANTIZE_EPSILON; + negYExtent += QUANTIZE_EPSILON; + if (negYExtent > posYExtent) { + negYExtent = posYExtent; + } + } + if (this.facing != ModelQuadFacing.POS_Z && this.facing != ModelQuadFacing.NEG_Z) { + posZExtent -= QUANTIZE_EPSILON; + negZExtent += QUANTIZE_EPSILON; + if (negZExtent > posZExtent) { + negZExtent = posZExtent; + } + } + + // POS_X, POS_Y, POS_Z, NEG_X, NEG_Y, NEG_Z + this.extents = new float[] { posXExtent, posYExtent, posZExtent, negXExtent, negYExtent, negZExtent }; + var uniqueVertexes = 4 - Integer.bitCount(sameVertexMap); + if ((!this.facing.isAligned() || uniqueVertexes != 4) && uniqueVertexes >= 3) { + var invUniqueVertexes = 1.0f / uniqueVertexes; + var centerX = xSum * invUniqueVertexes; + var centerY = ySum * invUniqueVertexes; + var centerZ = zSum * invUniqueVertexes; + this.center = new Vector3f(centerX, centerY, centerZ); + } + + return sameVertexMap; + } + + void initDotProduct() { if (this.facing.isAligned()) { this.accurateDotProduct = getAlignedDotProduct(this.facing, this.extents); } else { @@ -55,13 +168,7 @@ private static float getAlignedDotProduct(ModelQuadFacing facing, float[] extent return extents[facing.ordinal()] * facing.getSign(); } - static TQuad fromAligned(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center) { - return new TQuad(facing, extents, vertexPositions, center, ModelQuadFacing.PACKED_ALIGNED_NORMALS[facing.ordinal()]); - } - - static TQuad fromUnaligned(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center, int packedNormal) { - return new TQuad(facing, extents, vertexPositions, center, packedNormal); - } + public abstract float[] getVertexPositions(); public ModelQuadFacing getFacing() { return this.facing; @@ -76,7 +183,7 @@ public ModelQuadFacing useQuantizedFacing() { if (!this.facing.isAligned()) { // quantize the normal, get the new facing and get fix the dot product to match this.getQuantizedNormal(); - this.facing = ModelQuadFacing.fromNormal(this.quantizedNormal.x(), this.quantizedNormal.y(), this.quantizedNormal.z()); + this.facing = ModelQuadFacing.fromNormal(this.quantizedNormal); if (this.facing.isAligned()) { this.quantizedDotProduct = getAlignedDotProduct(this.facing, this.extents); } else { @@ -91,31 +198,6 @@ public float[] getExtents() { return this.extents; } - public float[] getVertexPositions() { - // calculate vertex positions from extents if there's no cached value - // (we don't want to be preemptively collecting vertex positions for all aligned quads) - if (this.vertexPositions == null) { - this.vertexPositions = new float[12]; - - var facingAxis = this.facing.getAxis(); - var xRange = facingAxis == 0 ? 0 : 3; - var yRange = facingAxis == 1 ? 0 : 3; - var zRange = facingAxis == 2 ? 0 : 3; - - var itemIndex = 0; - for (int x = 0; x <= xRange; x += 3) { - for (int y = 0; y <= yRange; y += 3) { - for (int z = 0; z <= zRange; z += 3) { - this.vertexPositions[itemIndex++] = this.extents[x]; - this.vertexPositions[itemIndex++] = this.extents[y + 1]; - this.vertexPositions[itemIndex++] = this.extents[z + 2]; - } - } - } - } - return this.vertexPositions; - } - public Vector3fc getCenter() { // calculate aligned quad center on demand if (this.center == null) { @@ -182,14 +264,14 @@ private void computeQuantizedNormal() { // in each axis the number of values is 2 * QUANTIZATION_FACTOR + 1. // the total number of normals is the number of points on that cube's surface. var normal = new Vector3f( - (int) (normX * QUANTIZATION_FACTOR), - (int) (normY * QUANTIZATION_FACTOR), - (int) (normZ * QUANTIZATION_FACTOR)); + (int) (normX * NORMAL_QUANTIZATION_STEPS), + (int) (normY * NORMAL_QUANTIZATION_STEPS), + (int) (normZ * NORMAL_QUANTIZATION_STEPS)); normal.normalize(); this.quantizedNormal = normal; } - int getQuadHash() { + public int getQuadHash() { // the hash code needs to be particularly collision resistant int result = 1; result = 31 * result + Arrays.hashCode(this.extents); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/DirectTriggers.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/DirectTriggers.java index 4c1943cb14..bde5812092 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/DirectTriggers.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/DirectTriggers.java @@ -1,20 +1,19 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; -import org.joml.Vector3d; -import org.joml.Vector3dc; - import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering.SectionTriggers; import net.minecraft.core.SectionPos; +import org.joml.Vector3d; +import org.joml.Vector3dc; /** * Performs direct triggering for sections that are sorted by distance. Direct * triggering means the section is not triggered based on its geometry but * rather the movement of the camera relative to the last position the camera * was in when the section was sorted last. - * + *

* There are two types of direct triggering: Distance triggering is used when * the camera is close to or inside the section. Angle triggering is used * otherwise. Distance triggering sorts the section when the camera has moved at @@ -27,7 +26,7 @@ class DirectTriggers implements SectionTriggers { * A tree map of the directly triggered sections, indexed by their * minimum required camera movement. When the given camera movement is exceeded, * they are tested for triggering the angle or distance condition. - * + *

* The accumulated distance is monotonically increasing and is never reset. This * only becomes a problem when the camera moves more than 10^15 blocks in total. * There will be precision issues at around 10^10 maybe, but it's still not a @@ -88,9 +87,9 @@ private static class DirectTriggerData { Vector3dc getSectionCenter() { if (this.sectionCenter == null) { this.sectionCenter = new Vector3d( - sectionPos.minBlockX() + 8, - sectionPos.minBlockY() + 8, - sectionPos.minBlockZ() + 8); + this.sectionPos.minBlockX() + 8, + this.sectionPos.minBlockY() + 8, + this.sectionPos.minBlockZ() + 8); } return this.sectionCenter; } @@ -111,16 +110,16 @@ Vector3dc getSectionCenter() { * section. */ double getSectionCenterTriggerCameraDist() { - return Math.sqrt(getSectionCenterDistSquared(this.triggerCameraPos)); + return Math.sqrt(this.getSectionCenterDistSquared(this.triggerCameraPos)); } double getSectionCenterDistSquared(Vector3dc vector) { - Vector3dc sectionCenter = getSectionCenter(); + Vector3dc sectionCenter = this.getSectionCenter(); return sectionCenter.distanceSquared(vector); } boolean isAngleTriggering(Vector3dc vector) { - return getSectionCenterDistSquared(vector) > SECTION_CENTER_DIST_SQUARED; + return this.getSectionCenterDistSquared(vector) > SECTION_CENTER_DIST_SQUARED; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GFNITriggers.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GFNITriggers.java index ee3f591f42..61ab847648 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GFNITriggers.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GFNITriggers.java @@ -1,12 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import org.joml.Vector3fc; - +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering.SectionTriggers; import net.minecraft.core.SectionPos; +import org.joml.Vector3fc; /** * Performs triggering based on globally-indexed face planes, bucketed by their normals. @@ -20,7 +20,7 @@ class GFNITriggers implements SectionTriggers { /** * A map of all the normal lists, indexed by their normal. */ - private Object2ReferenceOpenHashMap normalLists = new Object2ReferenceOpenHashMap<>(); + private final Object2ReferenceMap normalLists = new Object2ReferenceOpenHashMap<>(); int getUniqueNormalCount() { return this.normalLists.size(); @@ -33,11 +33,11 @@ public void processTriggers(SortTriggering ts, CameraMovement movement) { } } - private void addSectionInNewNormalLists(DynamicData dynamicData, NormalPlanes normalPlanes) { + private void addSectionInNewNormalLists(NormalPlanes normalPlanes) { var normal = normalPlanes.normal; var normalList = this.normalLists.get(normal); if (normalList == null) { - normalList = new NormalList(normal); + normalList = new NormalList(normal, normalPlanes.alignedDirection); this.normalLists.put(normal, normalList); normalList.addSection(normalPlanes, normalPlanes.sectionPos.asLong()); } @@ -94,14 +94,14 @@ public void integrateSection(SortTriggering ts, SectionPos pos, DynamicData data if (aligned != null) { for (var normalPlane : aligned) { if (normalPlane != null) { - this.addSectionInNewNormalLists(data, normalPlane); + this.addSectionInNewNormalLists(normalPlane); } } } var unaligned = geometryPlanes.getUnaligned(); if (unaligned != null) { for (var normalPlane : unaligned) { - this.addSectionInNewNormalLists(data, normalPlane); + this.addSectionInNewNormalLists(normalPlane); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java index 16acdf0230..cab8bef130 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java @@ -1,13 +1,14 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import java.util.Collection; - -import org.joml.Vector3fc; - +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.quad.TQuad; import net.minecraft.core.SectionPos; +import org.joml.Vector3f; +import org.joml.Vector3fc; + +import java.util.Collection; /** * GeometryPlanes stores the NormalPlanes for different normals, both aligned @@ -15,7 +16,8 @@ */ public class GeometryPlanes { private NormalPlanes[] alignedPlanes; - private Object2ReferenceOpenHashMap unalignedPlanes; + private Object2ReferenceMap unalignedPlanes; + private final Vector3f unalignedNormalScratch = new Vector3f(); public NormalPlanes[] getAligned() { return this.alignedPlanes; @@ -35,27 +37,20 @@ public Collection getUnaligned() { return this.unalignedPlanes.values(); } - public Object2ReferenceOpenHashMap getUnalignedOrCreate() { + public Object2ReferenceMap getUnalignedOrCreate() { if (this.unalignedPlanes == null) { this.unalignedPlanes = new Object2ReferenceOpenHashMap<>(); } return this.unalignedPlanes; } - public Collection getUnalignedNormals() { - if (this.unalignedPlanes == null) { - return null; - } - return this.unalignedPlanes.keySet(); - } - NormalPlanes getPlanesForNormal(NormalList normalList) { var normal = normalList.getNormal(); - if (normal.isAligned()) { + if (normalList.isAligned()) { if (this.alignedPlanes == null) { return null; } - return this.alignedPlanes[normal.getAlignedDirection()]; + return this.alignedPlanes[normalList.getAlignedDirection()]; } else { if (this.unalignedPlanes == null) { return null; @@ -74,21 +69,50 @@ public void addAlignedPlane(SectionPos sectionPos, int direction, float distance normalPlanes.addPlaneMember(distance); } - public void addDoubleSidedPlane(SectionPos sectionPos, int axis, float distance) { + public void addDoubleSidedAlignedPlane(SectionPos sectionPos, int axis, float distance) { this.addAlignedPlane(sectionPos, axis, distance); this.addAlignedPlane(sectionPos, axis + 3, -distance); } public void addUnalignedPlane(SectionPos sectionPos, Vector3fc normal, float distance) { var unalignedDistances = this.getUnalignedOrCreate(); - var normalPlanes = unalignedDistances.get(normal); + + // create a copy of the vector where -0 is turned into +0, + // this avoids non-equality when comparing normals where just the sign on 0 is different + var cleanedNormal = this.cleanNormal(normal); + + var normalPlanes = unalignedDistances.get(cleanedNormal); if (normalPlanes == null) { - normalPlanes = new NormalPlanes(sectionPos, normal); - unalignedDistances.put(normal, normalPlanes); + // construct new normal plane using the cleaned normal to make sure its .normal is zero-cleaned + normalPlanes = new NormalPlanes(sectionPos, new Vector3f(cleanedNormal)); + + // NOTE: importantly use the cleaned normal here, not the cleanedNormal, which is mutable + unalignedDistances.put(normalPlanes.normal, normalPlanes); } normalPlanes.addPlaneMember(distance); } + private Vector3f cleanNormal(Vector3fc normal) { + var cleanedNormal = this.unalignedNormalScratch.set(normal); + + // convert any occurrences of 0 into +0, note that -0.0f == 0.0f + if (cleanedNormal.x == 0.0f) { + cleanedNormal.x = 0.0f; + } + if (cleanedNormal.y == 0.0f) { + cleanedNormal.y = 0.0f; + } + if (cleanedNormal.z == 0.0f) { + cleanedNormal.z = 0.0f; + } + return cleanedNormal; + } + + public void addDoubleSidedUnalignedPlane(SectionPos sectionPos, Vector3fc normal, float distance) { + this.addUnalignedPlane(sectionPos, normal, distance); + this.addUnalignedPlane(sectionPos, normal.negate(new Vector3f()), -distance); + } + public void addQuadPlane(SectionPos sectionPos, TQuad quad) { var facing = quad.useQuantizedFacing(); if (facing.isAligned()) { @@ -98,7 +122,7 @@ public void addQuadPlane(SectionPos sectionPos, TQuad quad) { } } - private void prepareAndInsert(Object2ReferenceOpenHashMap distancesByNormal) { + private void prepareAndInsert(Object2ReferenceMap distancesByNormal) { if (this.alignedPlanes != null) { for (var normalPlanes : this.alignedPlanes) { if (normalPlanes != null) { @@ -117,7 +141,7 @@ public void prepareIntegration() { this.prepareAndInsert(null); } - public Object2ReferenceOpenHashMap prepareAndGetDistances() { + public Object2ReferenceMap prepareAndGetDistances() { var distancesByNormal = new Object2ReferenceOpenHashMap(10); this.prepareAndInsert(distancesByNormal); return distancesByNormal; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/Group.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/Group.java index d669504b95..46db7e91e7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/Group.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/Group.java @@ -1,8 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; import net.caffeinemc.mods.sodium.client.util.interval_tree.DoubleInterval; - -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.AlignableNormal; +import org.joml.Vector3fc; /** * A group represents a set of face planes of the same normal within a section. @@ -33,7 +32,7 @@ class Group { double baseDistance; - AlignableNormal normal; + Vector3fc normal; Group(NormalPlanes normalPlanes) { this.replaceWith(normalPlanes); @@ -50,7 +49,7 @@ void replaceWith(NormalPlanes normalPlanes) { private boolean planeTriggered(double start, double end) { return start < this.distances.getEnd() && end > this.distances.getStart() - && AlignableNormal.queryRange(this.facePlaneDistances, + && NormalList.queryRange(this.facePlaneDistances, (float) (start - this.baseDistance), (float) (end - this.baseDistance)); } @@ -65,11 +64,11 @@ void triggerRange(SortTriggering ts, double start, double end) { /** * A pretty good heuristic for equality of captured translucent geometry data. - * + *

* It assumes that if the size, bounds, and hash are equal, they are most likely * the same. We also know that the existing and new data is for the same section * position since the group was retrieved from the map for the right position. - * + *

* TODO: how common are collisions and are they bad? * If they are common, use second or different hash */ diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalList.java index b1ed8928d7..7a5e086710 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalList.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalList.java @@ -1,20 +1,19 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import java.util.Collection; - -import org.joml.Math; -import org.joml.Vector3dc; - +import it.unimi.dsi.fastutil.floats.FloatArrays; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArraySet; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.interval_tree.DoubleInterval; import net.caffeinemc.mods.sodium.client.util.interval_tree.Interval; import net.caffeinemc.mods.sodium.client.util.interval_tree.Interval.Bounded; import net.caffeinemc.mods.sodium.client.util.interval_tree.IntervalTree; +import org.joml.Vector3fc; -import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.ReferenceArraySet; -import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.AlignableNormal; +import java.util.Collection; /** * A normal list contains all the face planes that have the same normal. @@ -35,7 +34,8 @@ public class NormalList { /** * The normal of this normal list. */ - private final AlignableNormal normal; + private final Vector3fc normal; + private final int alignedDirection; /** * An interval tree of group intervals. Since this only stores intervals, the @@ -58,29 +58,32 @@ public class NormalList { /** * Constructs a new normal list with the given unit normal vector and aligned * normal index. - * - * @param normal The unit normal vector + * + * @param normal The unit normal vector */ - NormalList(AlignableNormal normal) { + NormalList(Vector3fc normal, int alignedDirection) { this.normal = normal; + this.alignedDirection = alignedDirection; } - public AlignableNormal getNormal() { + public Vector3fc getNormal() { return this.normal; } - private double normalDotDouble(Vector3dc v) { - return org.joml.Math.fma(this.normal.x, v.x(), - org.joml.Math.fma(this.normal.y, v.y(), - this.normal.z * v.z())); + public boolean isAligned() { + return this.alignedDirection != ModelQuadFacing.UNASSIGNED_ORDINAL; + } + + public int getAlignedDirection() { + return this.alignedDirection; } void processMovement(SortTriggering ts, CameraMovement movement) { // calculate the distance range of the movement with respect to the normal - double start = this.normalDotDouble(movement.start()); - double end = this.normalDotDouble(movement.end()); + double start = MathUtil.floatDoubleDot(this.normal, movement.start()); + double end = MathUtil.floatDoubleDot(this.normal, movement.end()); - // stop if the movement is reverse with regards to the normal + // stop if the movement is reverse in regard to the normal // since this means it's moving against the normal if (start >= end) { return; @@ -89,19 +92,21 @@ void processMovement(SortTriggering ts, CameraMovement movement) { // perform the interval query on the group intervals and resolve each interval // to the collection of groups it maps to var interval = new DoubleInterval(start, end, Bounded.CLOSED); - for (Interval groupInterval : intervalTree.query(interval)) { - for (Group group : groupsByInterval.get(groupInterval)) { + for (Interval groupInterval : this.intervalTree.query(interval)) { + for (Group group : this.groupsByInterval.get(groupInterval)) { group.triggerRange(ts, start, end); } } } void processCatchup(SortTriggering ts, CameraMovement movement, long sectionPos) { - double start = this.normalDotDouble(movement.start()); - double end = this.normalDotDouble(movement.end()); + double start = MathUtil.floatDoubleDot(this.normal, movement.start()); + double end = MathUtil.floatDoubleDot(this.normal, movement.end()); + if (start >= end) { return; } + var group = this.groupsBySection.get(sectionPos); if (group != null) { group.triggerRange(ts, start, end); @@ -174,4 +179,28 @@ void updateSection(NormalPlanes normalPlanes, long sectionPos) { group.replaceWith(normalPlanes); this.addGroupInterval(group); } + + public static boolean queryRange(float[] sortedDistances, float start, float end) { + // test that there is actually an entry in the query range + int result = FloatArrays.binarySearch(sortedDistances, start); + if (result < 0) { + // recover the insertion point + int insertionPoint = -result - 1; + if (insertionPoint >= sortedDistances.length) { + // no entry in the query range + return false; + } + + // check if the entry at the insertion point, which is the next one greater than + // the start value, is less than or equal to the end value + if (sortedDistances[insertionPoint] <= end) { + // there is an entry in the query range + return true; + } + } else { + // exact match, trigger + return true; + } + return false; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalPlanes.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalPlanes.java index 2ca59ea98f..124e2254e3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalPlanes.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/NormalPlanes.java @@ -1,15 +1,15 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import org.joml.Vector3fc; -import java.util.Arrays; - +import it.unimi.dsi.fastutil.floats.FloatOpenHashSet; +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; +import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFacing; +import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.interval_tree.DoubleInterval; import net.caffeinemc.mods.sodium.client.util.interval_tree.Interval.Bounded; - -import it.unimi.dsi.fastutil.floats.FloatOpenHashSet; -import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.AlignableNormal; import net.minecraft.core.SectionPos; +import org.joml.Vector3fc; + +import java.util.Arrays; /** * NormalPlanes represents planes by a normal and a list of distances. Initially they're @@ -17,7 +17,10 @@ */ public class NormalPlanes { final FloatOpenHashSet relativeDistancesSet = new FloatOpenHashSet(16); - final AlignableNormal normal; + + final Vector3fc normal; + final int alignedDirection; + final SectionPos sectionPos; float[] relativeDistances; // relative to the base distance @@ -25,25 +28,23 @@ public class NormalPlanes { long relDistanceHash; double baseDistance; - private NormalPlanes(SectionPos sectionPos, AlignableNormal normal) { + private NormalPlanes(SectionPos sectionPos, Vector3fc normal, int alignedDirection) { this.sectionPos = sectionPos; + this.normal = normal; + this.alignedDirection = alignedDirection; } public NormalPlanes(SectionPos sectionPos, Vector3fc normal) { - this(sectionPos, AlignableNormal.fromUnaligned(normal)); + this(sectionPos, normal, ModelQuadFacing.UNASSIGNED_ORDINAL); } public NormalPlanes(SectionPos sectionPos, int alignedDirection) { - this(sectionPos, AlignableNormal.fromAligned(alignedDirection)); - } - - boolean addPlaneMember(float vertexX, float vertexY, float vertexZ) { - return this.addPlaneMember(this.normal.dot(vertexX, vertexY, vertexZ)); + this(sectionPos, ModelQuadFacing.ALIGNED_NORMALS[alignedDirection], alignedDirection); } - public boolean addPlaneMember(float distance) { - return this.relativeDistancesSet.add(distance); + public void addPlaneMember(float distance) { + this.relativeDistancesSet.add(distance); } public void prepareIntegration() { @@ -56,7 +57,8 @@ public void prepareIntegration() { var size = this.relativeDistancesSet.size(); this.relativeDistances = new float[this.relativeDistancesSet.size()]; int i = 0; - for (float relDistance : this.relativeDistancesSet) { + for (var it = this.relativeDistancesSet.iterator(); it.hasNext(); ) { + float relDistance = it.nextFloat(); this.relativeDistances[i++] = relDistance; long distanceBits = Double.doubleToLongBits(relDistance); @@ -64,17 +66,17 @@ public void prepareIntegration() { } // sort the array ascending - Arrays.sort(relativeDistances); + Arrays.sort(this.relativeDistances); - this.baseDistance = this.normal.dot( - sectionPos.minBlockX(), sectionPos.minBlockY(), sectionPos.minBlockZ()); + // make sure to use double-based dot product math here to prevent incorrect sort triggering + this.baseDistance = MathUtil.floatDoubleDot(this.normal, this.sectionPos.minBlockX(), this.sectionPos.minBlockY(), this.sectionPos.minBlockZ()); this.distanceRange = new DoubleInterval( this.relativeDistances[0] + this.baseDistance, this.relativeDistances[size - 1] + this.baseDistance, Bounded.CLOSED); } - public void prepareAndInsert(Object2ReferenceOpenHashMap distancesByNormal) { + public void prepareAndInsert(Object2ReferenceMap distancesByNormal) { this.prepareIntegration(); if (distancesByNormal != null) { distancesByNormal.put(this.normal, this.relativeDistances); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/SortTriggering.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/SortTriggering.java index b58cd10aa7..cf37e6e2e7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/SortTriggering.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/SortTriggering.java @@ -1,25 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger; -import java.util.List; -import java.util.function.BiConsumer; - -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; -import org.joml.Vector3dc; - import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.caffeinemc.mods.sodium.client.SodiumClientMod; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.AlignableNormal; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.minecraft.core.SectionPos; +import org.joml.Vector3dc; +import org.joml.Vector3fc; + +import java.util.List; +import java.util.function.BiConsumer; /** * This class is a central point in translucency sorting. It counts the number * of translucent data objects for each sort type and delegates triggering of * sections for dynamic sorting to the trigger components. - * + *

* TODO: * - investigate why there's a similar number of STA and DYN sections. This might be normal, the counters might be broken or the heuristic is actually wrong. * @@ -30,7 +29,7 @@ public class SortTriggering { * To avoid generating a collection of the triggered sections, this callback is * used to process the triggered sections directly as they are queried from the * normal lists' interval trees. The callback is given the section coordinates, - * and a boolean indicating if the trigger was an direct trigger. + * and a boolean indicating if the trigger was a direct trigger. */ private BiConsumer triggerSectionCallback; @@ -39,7 +38,7 @@ public class SortTriggering { * later) it might not have the required trigger data registered yet so that it * might miss being triggered between being scheduled for rebuild and being * integrated. This is solved by catching up the section being integrated with - * the movement that has happened in the mean time. + * the movement that has happened in the meantime. */ private DynamicData catchupData = null; @@ -49,7 +48,7 @@ public class SortTriggering { */ private int gfniTriggerCount = 0; private int directTriggerCount = 0; - private final ObjectOpenHashSet triggeredNormals = new ObjectOpenHashSet<>(); + private final ObjectOpenHashSet triggeredNormals = new ObjectOpenHashSet<>(); private int triggeredNormalCount = 0; /** @@ -99,7 +98,7 @@ private boolean isCatchingUp() { return this.catchupData != null; } - void triggerSectionGFNI(long sectionPos, AlignableNormal normal) { + void triggerSectionGFNI(long sectionPos, Vector3fc normal) { if (this.isCatchingUp()) { this.triggerSectionCatchup(sectionPos, false); return; @@ -124,7 +123,7 @@ private void triggerSectionCatchup(long sectionPos, boolean isDirectTrigger) { // catchup triggering might be disabled if (this.triggerSectionCallback != null) { // do prepareTrigger here since it can't be done through the render section as - // it hasn't been put there yet or it contains an old data object + // it hasn't been put there yet, or it contains an old data object this.catchupData.prepareTrigger(isDirectTrigger); // schedule the section to be re-sorted @@ -223,23 +222,20 @@ public void integrateTranslucentData(TranslucentData oldData, TranslucentData ne } } - public void addDebugStrings(List list) { - var sortBehavior = SodiumClientMod.options().performance.getSortBehavior(); - if (sortBehavior.getSortMode() == SortBehavior.SortMode.NONE) { - list.add("TS OFF"); - } else { - list.add("TS (%s) NL=%02d TrN=%02d TrS=G%03d/D%03d".formatted( - sortBehavior.getShortName(), - this.gfni.getUniqueNormalCount(), - this.triggeredNormalCount, - this.gfniTriggerCount, - this.directTriggerCount)); - list.add("N=%05d SNR=%05d STA=%05d DYN=%05d (DIR=%02d)".formatted( - this.sortTypeCounters[SortType.NONE.ordinal()], - this.sortTypeCounters[SortType.STATIC_NORMAL_RELATIVE.ordinal()], - this.sortTypeCounters[SortType.STATIC_TOPO.ordinal()], - this.sortTypeCounters[SortType.DYNAMIC.ordinal()], - this.direct.getDirectTriggerCount())); - } + public void addDebugStrings(List list, SortBehavior sortBehavior) { + var splittingMode = SodiumClientMod.options().performance.quadSplittingMode; + list.add("TS (%s,%s) NL=%02d TrN=%02d TrS=G%03d/D%03d".formatted( + sortBehavior.getShortName(), + splittingMode.getShortName(), + this.gfni.getUniqueNormalCount(), + this.triggeredNormalCount, + this.gfniTriggerCount, + this.directTriggerCount)); + list.add("N=%05d SNR=%05d STA=%05d DYN=%05d (DIR=%02d)".formatted( + this.sortTypeCounters[SortType.NONE.ordinal()], + this.sortTypeCounters[SortType.STATIC_NORMAL_RELATIVE.ordinal()], + this.sortTypeCounters[SortType.STATIC_TOPO.ordinal()], + this.sortTypeCounters[SortType.DYNAMIC.ordinal()], + this.direct.getDirectTriggerCount())); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java new file mode 100644 index 0000000000..e276f6d6e8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.CoordinateSectionVisitor; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class AbstractTraversableBiForest extends BaseBiForest implements TraversableForest { + public AbstractTraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void prepareForTraversal() { + this.mainTree.prepareForTraversal(); + if (this.secondaryTree != null) { + this.secondaryTree.prepareForTraversal(); + } + } + + @Override + public void traverse(CoordinateSectionVisitor visitor, Viewport viewport, float distanceLimit) { + // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree + this.mainTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + if (this.secondaryTree != null) { + this.secondaryTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java new file mode 100644 index 0000000000..e6b5b406df --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -0,0 +1,53 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.CoordinateSectionVisitor; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class AbstractTraversableMultiForest extends BaseMultiForest implements TraversableForest { + public AbstractTraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void prepareForTraversal() { + for (var tree : this.trees) { + if (tree != null) { + tree.prepareForTraversal(); + } + } + } + + @Override + public void traverse(CoordinateSectionVisitor visitor, Viewport viewport, float distanceLimit) { + var cameraPos = viewport.getChunkCoord(); + var cameraSectionX = cameraPos.getX(); + var cameraSectionY = cameraPos.getY(); + var cameraSectionZ = cameraPos.getZ(); + + // sort the trees by distance from the camera by sorting a packed index array. + var items = new int[this.trees.length]; + for (int i = 0; i < this.trees.length; i++) { + var tree = this.trees[i]; + if (tree != null) { + var deltaX = Math.abs(tree.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(tree.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(tree.offsetZ + 32 - cameraSectionZ); + items[i] = (deltaX + deltaY + deltaZ + 1) << 16 | i; + } + } + + IntArrays.unstableSort(items); + + // traverse in sorted front-to-back order for correct render order + for (var item : items) { + if (item == 0) { + continue; + } + var tree = this.trees[item & 0xFFFF]; + if (tree != null) { + tree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + } + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java new file mode 100644 index 0000000000..9fa4828d9d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java @@ -0,0 +1,59 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.world.level.Level; + +public abstract class BaseBiForest extends BaseForest { + private static final int SECONDARY_TREE_OFFSET_XZ = 4; + private static final int MAX_BUILD_DISTANCE = 16 * 65 / 2; // radius of 32 chunk render distance + + protected final T mainTree; + protected T secondaryTree; + + public BaseBiForest(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.mainTree = this.makeTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + protected T makeSecondaryTree() { + // offset diagonally to fully encompass the required 65x65 area + return this.makeTree( + this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, + this.baseOffsetY, + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); + } + + @Override + public void add(int x, int y, int z) { + if (this.mainTree.add(x, y, z)) { + return; + } + + if (this.secondaryTree == null) { + this.secondaryTree = this.makeSecondaryTree(); + } + this.secondaryTree.add(x, y, z); + } + + @Override + public int getPresence(int x, int y, int z) { + var result = this.mainTree.getPresence(x, y, z); + if (result != Tree.OUT_OF_BOUNDS) { + return result; + } + + if (this.secondaryTree != null) { + return this.secondaryTree.getPresence(x, y, z); + } + return Tree.OUT_OF_BOUNDS; + } + + public static boolean checkApplicable(float buildDistance, Level level) { + int buildDistanceInt = (int) Math.ceil(buildDistance); + if (buildDistanceInt > MAX_BUILD_DISTANCE) { + return false; + } + + return level.getHeight() >> 4 <= 64; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java new file mode 100644 index 0000000000..2501fbd68e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java @@ -0,0 +1,15 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseForest implements Forest { + protected final int baseOffsetX, baseOffsetY, baseOffsetZ; + final float buildDistance; + + protected BaseForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + this.baseOffsetX = baseOffsetX; + this.baseOffsetY = baseOffsetY; + this.baseOffsetZ = baseOffsetZ; + this.buildDistance = buildDistance; + } + + protected abstract T makeTree(int offsetX, int offsetY, int offsetZ); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java new file mode 100644 index 0000000000..ccf62c58f9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -0,0 +1,91 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseMultiForest extends BaseForest { + protected final T[] trees; + protected final int forestDim; + + protected T lastTree; + + public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.forestDim = forestDimFromBuildDistance(buildDistance); + this.trees = this.makeTrees(this.forestDim * this.forestDim * this.forestDim); + } + + public static int forestDimFromBuildDistance(float buildDistance) { + // / 16 (block to chunk) * 2 (radius to diameter) + 1 (center chunk) / 64 (chunks per tree) + return (int) Math.ceil((buildDistance / 8.0 + 1) / 64.0); + } + + protected int getTreeIndex(int localX, int localY, int localZ) { + var treeX = localX >> 6; + var treeY = localY >> 6; + var treeZ = localZ >> 6; + + if (treeX < 0 || treeX >= this.forestDim || + treeY < 0 || treeY >= this.forestDim || + treeZ < 0 || treeZ >= this.forestDim) { + return Tree.OUT_OF_BOUNDS; + } + + return treeX + (treeZ * this.forestDim + treeY) * this.forestDim; + } + + @Override + public void add(int x, int y, int z) { + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return; + } + + var tree = this.trees[treeIndex]; + + if (tree == null) { + var treeOffsetX = this.baseOffsetX + (localX & ~0b111111); + var treeOffsetY = this.baseOffsetY + (localY & ~0b111111); + var treeOffsetZ = this.baseOffsetZ + (localZ & ~0b111111); + tree = this.makeTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees[treeIndex] = tree; + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + @Override + public int getPresence(int x, int y, int z) { + if (this.lastTree != null) { + var result = this.lastTree.getPresence(x, y, z); + if (result != Tree.OUT_OF_BOUNDS) { + return result; + } + } + + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return Tree.OUT_OF_BOUNDS; + } + + var tree = this.trees[treeIndex]; + if (tree != null) { + this.lastTree = tree; + return tree.getPresence(x, y, z); + } + return Tree.OUT_OF_BOUNDS; + } + + protected abstract T[] makeTrees(int length); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java new file mode 100644 index 0000000000..766e3e0687 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public interface Forest { + void add(int x, int y, int z); + + default void add(RenderSection section) { + this.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + int getPresence(int x, int y, int z); + + default boolean isSectionPresent(int x, int y, int z) { + return this.getPresence(x, y, z) == Tree.PRESENT; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java new file mode 100644 index 0000000000..4729c43b26 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java @@ -0,0 +1,5 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public interface RemovableForest extends TraversableForest { + void remove(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java new file mode 100644 index 0000000000..a81c219117 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java @@ -0,0 +1,139 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.CoordinateSectionVisitor; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; + +import java.util.Comparator; + +public class RemovableMultiForest implements RemovableForest { + private final Long2ReferenceLinkedOpenHashMap trees; + private final ReferenceArrayList treeSortList = new ReferenceArrayList<>(); + private RemovableTree lastTree; + + // the removable tree separately tracks if it needs to prepared for traversal because it's not just built once, prepared, and then traversed. Since it can receive updates, it needs to be prepared for traversal again and to avoid unnecessary preparation, it tracks whether it's ready. + private boolean treesAreReady = true; + + public RemovableMultiForest(float buildDistance) { + this.trees = new Long2ReferenceLinkedOpenHashMap<>(getCapacity(buildDistance)); + } + + private static int getCapacity(float buildDistance) { + var forestDim = BaseMultiForest.forestDimFromBuildDistance(buildDistance) + 1; + return forestDim * forestDim * forestDim; + } + + public void ensureCapacity(float buildDistance) { + this.trees.ensureCapacity(getCapacity(buildDistance)); + } + + @Override + public void prepareForTraversal() { + if (this.treesAreReady) { + return; + } + + var it = this.trees.values().iterator(); + while (it.hasNext()) { + var tree = it.next(); + tree.prepareForTraversal(); + if (tree.isEmpty()) { + it.remove(); + if (this.lastTree == tree) { + this.lastTree = null; + } + } + } + + this.treesAreReady = true; + } + + @Override + public void traverse(CoordinateSectionVisitor visitor, Viewport viewport, float distanceLimit) { + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + + // sort the trees by distance from the camera by sorting a packed index array. + this.treeSortList.clear(); + this.treeSortList.ensureCapacity(this.trees.size()); + this.treeSortList.addAll(this.trees.values()); + for (var tree : this.treeSortList) { + tree.updateSortKeyFor(cameraSectionX, cameraSectionY, cameraSectionZ); + } + + this.treeSortList.unstableSort(Comparator.comparingInt(RemovableTree::getSortKey)); + + // traverse in sorted front-to-back order for correct render order + for (var tree : this.treeSortList) { + // disable distance test in traversal because we don't use it here + tree.traverse(visitor, viewport, 0, 0); + } + } + + @Override + public void add(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + var treeOffsetX = treeX << 6; + var treeOffsetY = treeY << 6; + var treeOffsetZ = treeZ << 6; + tree = new RemovableTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees.put(treeKey, tree); + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + public void remove(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.remove(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + return; + } + + tree.remove(x, y, z); + + this.lastTree = tree; + } + + public void remove(RenderSection section) { + this.remove(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + @Override + public int getPresence(int x, int y, int z) { + // unused operation on removable trees + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java new file mode 100644 index 0000000000..4b7cea63e4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java @@ -0,0 +1,69 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.core.SectionPos; + +public class RemovableTree extends TraversableTree { + private boolean reducedIsValid = true; + private int sortKey; + + public RemovableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public boolean remove(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] &= ~(1L << (bitIndex & 0b111111)); + + this.reducedIsValid = false; + + return true; + } + + @Override + public void prepareForTraversal() { + if (!this.reducedIsValid) { + super.prepareForTraversal(); + this.reducedIsValid = true; + } + } + + @Override + public boolean add(int x, int y, int z) { + var result = super.add(x, y, z); + if (result) { + this.reducedIsValid = false; + } + return result; + } + + public boolean isEmpty() { + return this.treeDoubleReduced == 0; + } + + public long getTreeKey() { + return SectionPos.asLong(this.offsetX, this.offsetY, this.offsetZ); + } + + public void updateSortKeyFor(int cameraSectionX, int cameraSectionY, int cameraSectionZ) { + var deltaX = Math.abs(this.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(this.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(this.offsetZ + 32 - cameraSectionZ); + this.sortKey = deltaX + deltaY + deltaZ + 1; + } + + public int getSortKey() { + return this.sortKey; + } + + @Override + public int getPresence(int i, int i1, int i2) { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java new file mode 100644 index 0000000000..ae75cc441b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public class TraversableBiForest extends AbstractTraversableBiForest { + public TraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java new file mode 100644 index 0000000000..bcd38e8595 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.CoordinateSectionVisitor; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public interface TraversableForest extends Forest { + void prepareForTraversal(); + + void traverse(CoordinateSectionVisitor visitor, Viewport viewport, float distanceLimit); + + static TraversableForest createTraversableForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { + if (BaseBiForest.checkApplicable(buildDistance, level)) { + return new TraversableBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + return new TraversableMultiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java new file mode 100644 index 0000000000..d547655de8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public class TraversableMultiForest extends AbstractTraversableMultiForest { + public TraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } + + @Override + protected TraversableTree[] makeTrees(int length) { + return new TraversableTree[length]; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java new file mode 100644 index 0000000000..3e4f32b392 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -0,0 +1,301 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.CoordinateSectionVisitor; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import org.joml.FrustumIntersection; + +/** + * A traversable tree is a tree of sections that can be traversed with a distance limit and a frustum. It traverses the sections in visual front-to-back order, so that they can be directly put into a render list. Note however that ordering regions by adding them to the list the first time one of their sections is visited does not yield the correct order. This is because the sections are traversed in visual order, not ordered by distance from the camera. + */ +public class TraversableTree extends Tree { + private static final int INSIDE_FRUSTUM = 0b01; + private static final int INSIDE_DISTANCE = 0b10; + private static final int FULLY_INSIDE = INSIDE_FRUSTUM | INSIDE_DISTANCE; + + protected final long[] treeReduced = new long[64]; + public long treeDoubleReduced = 0L; + + // set temporarily during traversal + private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private CoordinateSectionVisitor visitor; + protected Viewport viewport; + private float distanceLimit; + + public TraversableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public void prepareForTraversal() { + long doubleReduced = 0; + for (int i = 0; i < 64; i++) { + long reduced = 0; + var reducedOffset = i << 6; + for (int j = 0; j < 64; j++) { + reduced |= this.tree[reducedOffset + j] == 0 ? 0L : 1L << j; + } + this.treeReduced[i] = reduced; + doubleReduced |= reduced == 0 ? 0L : 1L << i; + } + this.treeDoubleReduced = doubleReduced; + } + + @Override + public int getPresence(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (isOutOfBounds(x, y, z)) { + return OUT_OF_BOUNDS; + } + + var bitIndex = interleave6x3(x, y, z); + int doubleReducedBitIndex = bitIndex >> 12; + if ((this.treeDoubleReduced & (1L << doubleReducedBitIndex)) == 0) { + return NOT_PRESENT; + } + + int reducedBitIndex = bitIndex >> 6; + return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0 ? PRESENT : NOT_PRESENT; + } + + public void traverse(CoordinateSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + this.visitor = visitor; + this.viewport = viewport; + this.distanceLimit = distanceLimit; + + // + 1 to offset section position to compensate for shifted global offset + // adjust camera block position to account for fractional part of camera position + var sectionPos = viewport.getChunkCoord(); + this.cameraOffsetX = sectionPos.getX() - this.offsetX + 1; + this.cameraOffsetY = sectionPos.getY() - this.offsetY + 1; + this.cameraOffsetZ = sectionPos.getZ() - this.offsetZ + 1; + + // everything is already inside the distance limit if the build distance is smaller + var initialInside = this.distanceLimit >= buildDistance ? INSIDE_DISTANCE : 0; + this.traverse(this.getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); + + this.visitor = null; + this.viewport = null; + } + + void traverse(int orderModulator, int nodeOrigin, int level, int inside) { + // half of the dimension of a child of this node, in blocks + int childHalfDim = 1 << (level + 3); // * 16 / 2 + + // odd levels (the higher levels of each reduction) need to modulate indexes that are multiples of 8 + if ((level & 1) == 1) { + orderModulator <<= 3; + } + + if (level <= 1) { + // check using the full bitmap + int childOriginBase = nodeOrigin & 0b111111_111111_000000; + long map = this.tree[nodeOrigin >> 6]; + + if (level == 0) { + int startBit = nodeOrigin & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + int sectionOrigin = childOriginBase | childIndex; + int x = deinterleave6(sectionOrigin) + this.offsetX; + int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; + int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; + + if (inside == FULLY_INSIDE || this.testLeafNode(x, y, z, inside)) { + this.visitor.visit(x, y, z); + } + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); + } + } + } + } else if (level <= 3) { + int childOriginBase = nodeOrigin & 0b111111_000000_000000; + long map = this.treeReduced[nodeOrigin >> 12]; + + if (level == 2) { + int startBit = (nodeOrigin >> 6) & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } + } else { + if (level == 4) { + int startBit = nodeOrigin >> 12; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (0xFFL << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } + } + } + + void testChild(int childOrigin, int childHalfDim, int level, int inside) { + // calculate section coordinates in tree-space + int x = deinterleave6(childOrigin); + int y = deinterleave6(childOrigin >> 1); + int z = deinterleave6(childOrigin >> 2); + + // immediately traverse if fully inside + if (inside == FULLY_INSIDE) { + level--; + this.traverse(this.getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + return; + } + + // convert to world-space section origin in blocks, then to camera space + var transform = this.viewport.getTransform(); + int worldX = ((x + this.offsetX) << 4) - transform.intX; + int worldY = ((y + this.offsetY) << 4) - transform.intY; + int worldZ = ((z + this.offsetZ) << 4) - transform.intZ; + + boolean visible = true; + + if ((inside & INSIDE_FRUSTUM) == 0) { + var intersectionResult = this.viewport.getBoxIntersectionDirect( + (worldX + childHalfDim) - transform.fracX, + (worldY + childHalfDim) - transform.fracY, + (worldZ + childHalfDim) - transform.fracZ, + childHalfDim + Viewport.CHUNK_SECTION_MARGIN); + if (intersectionResult == FrustumIntersection.INSIDE) { + inside |= INSIDE_FRUSTUM; + } else { + visible = intersectionResult == FrustumIntersection.INTERSECT; + } + } + + if ((inside & INSIDE_DISTANCE) == 0) { + // calculate the point of the node closest to the camera + int childFullDim = childHalfDim << 1; + float dx = nearestToZero(worldX, worldX + childFullDim) - transform.fracX; + float dy = nearestToZero(worldY, worldY + childFullDim) - transform.fracY; + float dz = nearestToZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + // check if closest point inside the cylinder + visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + if (visible) { + // if the farthest point is also visible, the node is fully inside + dx = farthestFromZero(worldX, worldX + childFullDim) - transform.fracX; + dy = farthestFromZero(worldY, worldY + childFullDim) - transform.fracY; + dz = farthestFromZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { + inside |= INSIDE_DISTANCE; + } + } + } + + if (visible) { + level--; + this.traverse(this.getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + } + } + + boolean testLeafNode(int x, int y, int z, int inside) { + // input coordinates are section coordinates in world-space + + var transform = this.viewport.getTransform(); + + // convert to blocks and move into integer camera space + x = (x << 4) - transform.intX; + y = (y << 4) - transform.intY; + z = (z << 4) - transform.intZ; + + // test frustum if not already inside frustum + if ((inside & INSIDE_FRUSTUM) == 0 && !this.viewport.isBoxVisibleDirect( + (x + 8) - transform.fracX, + (y + 8) - transform.fracY, + (z + 8) - transform.fracZ, + Viewport.CHUNK_SECTION_PADDED_RADIUS)) { + return false; + } + + // test distance if not already inside distance + if ((inside & INSIDE_DISTANCE) == 0) { + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(x - 1, x + 17) - transform.fracX; + float dy = nearestToZero(y - 1, y + 17) - transform.fracY; + float dz = nearestToZero(z - 1, z + 17) - transform.fracZ; + + return cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + } + + return true; + } + + static boolean cylindricalDistanceTest(float dx, float dy, float dz, float distanceLimit) { + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (distanceLimit * distanceLimit)) && + (Math.abs(dy) < distanceLimit); + } + + @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. + private static int nearestToZero(int min, int max) { + // this compiles to slightly better code than Math.min(Math.max(0, min), max) + int clamped = 0; + if (min > 0) { + clamped = min; + } + if (max < 0) { + clamped = max; + } + return clamped; + } + + private static int farthestFromZero(int min, int max) { + int clamped = 0; + if (min > 0) { + clamped = max; + } + if (max < 0) { + clamped = min; + } + if (clamped == 0) { + if (Math.abs(min) > Math.abs(max)) { + clamped = min; + } else { + clamped = max; + } + } + return clamped; + } + + int getChildOrderModulator(int x, int y, int z, int childFullSectionDim) { + return (x + childFullSectionDim - this.cameraOffsetX) >>> 31 + | ((y + childFullSectionDim - this.cameraOffsetY) >>> 31) << 1 + | ((z + childFullSectionDim - this.cameraOffsetZ) >>> 31) << 2; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java new file mode 100644 index 0000000000..9128db2721 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java @@ -0,0 +1,54 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class Tree { + public static final int OUT_OF_BOUNDS = -1; + public static final int NOT_PRESENT = 0; + public static final int PRESENT = 1; + + protected final long[] tree = new long[64 * 64]; + protected final int offsetX, offsetY, offsetZ; + + public Tree(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + public static boolean isOutOfBounds(int x, int y, int z) { + return x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0; + } + + protected static int interleave6x3(int x, int y, int z) { + return Tree.interleave6(x) | Tree.interleave6(y) << 1 | Tree.interleave6(z) << 2; + } + + private static int interleave6(int n) { + n &= 0b000000000000111111; + n = (n | n << 4 | n << 8) & 0b000011000011000011; + n = (n | n << 2) & 0b001001001001001001; + return n; + } + + protected static int deinterleave6(int n) { + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; + n = (n | n >> 4 | n >> 8) & 0b000000000000111111; + return n; + } + + public boolean add(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); + + return true; + } + + public abstract int getPresence(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java index 0b9771d600..a68cb975b1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java @@ -3,8 +3,8 @@ import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; -import org.apache.commons.lang3.Validate; import org.lwjgl.system.MemoryUtil; + import java.nio.ByteBuffer; public class ChunkMeshBufferBuilder { @@ -45,6 +45,11 @@ public void push(ChunkVertexEncoder.Vertex[] vertices, int materialBits) { this.vertexCount += 4; } + public void writeExternal(ByteBuffer buffer, int position, ChunkVertexEncoder.Vertex[] vertices, Material material) { + this.encoder.write(MemoryUtil.memAddress(buffer, position * this.stride), + material.bits(), vertices, this.sectionIndex); + } + private void ensureCapacity(int vertexCount) { if (this.vertexCount + vertexCount >= this.vertexCapacity) { this.grow(vertexCount); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java index cac465580c..3f6db6e205 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java @@ -22,5 +22,27 @@ public static Vertex[] uninitializedQuad() { return vertices; } + + public static void copyVertexTo(Vertex from, Vertex to) { + to.x = from.x; + to.y = from.y; + to.z = from.z; + to.color = from.color; + to.ao = from.ao; + to.u = from.u; + to.v = from.v; + to.light = from.light; + } + + public static void writeVertex(ChunkVertexEncoder.Vertex targetA, float newX, float newY, float newZ, int newColor, float newAo, float newU, float newV, int newLight) { + targetA.x = newX; + targetA.y = newY; + targetA.z = newZ; + targetA.color = newColor; + targetA.ao = newAo; + targetA.u = newU; + targetA.v = newV; + targetA.light = newLight; + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java index 754f09946b..ab191b9934 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java @@ -1,10 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.impl; +import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.client.gl.attribute.GlVertexFormat; import net.caffeinemc.mods.sodium.client.render.chunk.shader.ChunkShaderBindingPoints; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; -import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.minecraft.util.Mth; import org.lwjgl.system.MemoryUtil; @@ -18,8 +18,8 @@ public class CompactChunkVertex implements ChunkVertexType { .addElement(DefaultChunkMeshAttributes.LIGHT_MATERIAL_INDEX, ChunkShaderBindingPoints.ATTRIBUTE_LIGHT_MATERIAL_INDEX, 16) .build(); - private static final int POSITION_MAX_VALUE = 1 << 20; - private static final int TEXTURE_MAX_VALUE = 1 << 15; + public static final int POSITION_MAX_VALUE = 1 << 20; + public static final int TEXTURE_MAX_VALUE = 1 << 15; private static final float MODEL_ORIGIN = 8.0f; private static final float MODEL_RANGE = 32.0f; @@ -58,7 +58,7 @@ public ChunkVertexEncoder getEncoder() { MemoryUtil.memPutInt(ptr + 0L, packPositionHi(x, y, z)); MemoryUtil.memPutInt(ptr + 4L, packPositionLo(x, y, z)); - MemoryUtil.memPutInt(ptr + 8L, ColorHelper.multiplyRGB(vertex.color, vertex.ao)); + MemoryUtil.memPutInt(ptr + 8L, ColorARGB.mulRGB(vertex.color, vertex.ao)); MemoryUtil.memPutInt(ptr + 12L, packTexture(u, v)); MemoryUtil.memPutInt(ptr + 16L, packLightAndData(light, materialBits, section)); @@ -101,7 +101,7 @@ private static int encodeTexture(float center, float x) { // This makes it possible to use much smaller epsilons for avoiding texture bleed, since the epsilon is no // longer encoded into the vertex data (instead, we only store the sign.) int bias = (x < center) ? 1 : -1; - int quantized = floorInt(x * TEXTURE_MAX_VALUE) + bias; + int quantized = Math.round(x * TEXTURE_MAX_VALUE) + bias; return (quantized & 0x7FFF) | (sign(bias) << 15); } @@ -125,7 +125,4 @@ private static int sign(int x) { return (x >>> 31); } - private static int floorInt(float x) { - return (int) Math.floor(x); - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/SodiumRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/SodiumRenderer.java index 7b39a52063..e2ff77cea7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/SodiumRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/SodiumRenderer.java @@ -55,15 +55,15 @@ public MaterialFinder materialFinder() { @Override public RenderMaterial materialById(ResourceLocation id) { - return materialMap.get(id); + return this.materialMap.get(id); } @Override public boolean registerMaterial(ResourceLocation id, RenderMaterial material) { - if (materialMap.containsKey(id)) return false; + if (this.materialMap.containsKey(id)) return false; // cast to prevent acceptance of impostor implementations - materialMap.put(id, (RenderMaterialImpl) material); + this.materialMap.put(id, (RenderMaterialImpl) material); return true; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/helper/ColorHelper.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/helper/ColorHelper.java index 9961263598..2acf626078 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/helper/ColorHelper.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/helper/ColorHelper.java @@ -16,7 +16,8 @@ package net.caffeinemc.mods.sodium.client.render.frapi.helper; -import java.nio.ByteOrder; +import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.api.util.ColorARGB; /** * Static routines of general utility for renderer implementations. @@ -24,44 +25,9 @@ * designed to be usable without the default renderer. */ public abstract class ColorHelper { - private ColorHelper() { } - - private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; - - /** Component-wise multiply. Components need to be in same order in both inputs! */ - public static int multiplyColor(int color1, int color2) { - if (color1 == -1) { - return color2; - } else if (color2 == -1) { - return color1; - } - - final int alpha = ((color1 >>> 24) & 0xFF) * ((color2 >>> 24) & 0xFF) / 0xFF; - final int red = ((color1 >>> 16) & 0xFF) * ((color2 >>> 16) & 0xFF) / 0xFF; - final int green = ((color1 >>> 8) & 0xFF) * ((color2 >>> 8) & 0xFF) / 0xFF; - final int blue = (color1 & 0xFF) * (color2 & 0xFF) / 0xFF; - - return (alpha << 24) | (red << 16) | (green << 8) | blue; - } - - /** Multiplies three lowest components by shade. High byte (usually alpha) unchanged. */ - public static int multiplyRGB(int color, float shade) { - final int alpha = ((color >>> 24) & 0xFF); - final int red = (int) (((color >>> 16) & 0xFF) * shade); - final int green = (int) (((color >>> 8) & 0xFF) * shade); - final int blue = (int) ((color & 0xFF) * shade); - - return (alpha << 24) | (red << 16) | (green << 8) | blue; - } - - /** - * Component-wise max. - */ public static int maxBrightness(int b0, int b1) { - if (b0 == 0) return b1; - if (b1 == 0) return b0; - - return Math.max(b0 & 0xFFFF, b1 & 0xFFFF) | Math.max(b0 & 0xFFFF0000, b1 & 0xFFFF0000); + return Math.max(b0 & 0x0000FFFF, b1 & 0x0000FFFF) | + Math.max(b0 & 0xFFFF0000, b1 & 0xFFFF0000); } /* @@ -81,36 +47,16 @@ Vanilla color format (big endian): RGBA (0xRRGGBBAA) */ /** - * Converts from ARGB color to ABGR color if little endian or RGBA color if big endian. + * Converts from ARGB color to ABGR color. The result will be in the platform's native byte order. */ public static int toVanillaColor(int color) { - if (color == -1) { - return -1; - } - - if (BIG_ENDIAN) { - // ARGB to RGBA - return ((color & 0x00FFFFFF) << 8) | ((color & 0xFF000000) >>> 24); - } else { - // ARGB to ABGR - return (color & 0xFF00FF00) | ((color & 0x00FF0000) >>> 16) | ((color & 0x000000FF) << 16); - } + return ColorABGR.toNativeByteOrder(ColorARGB.toABGR(color)); } /** - * Converts to ARGB color from ABGR color if little endian or RGBA color if big endian. + * Converts from ABGR color to ARGB color. The input should be in the platform's native byte order. */ public static int fromVanillaColor(int color) { - if (color == -1) { - return -1; - } - - if (BIG_ENDIAN) { - // RGBA to ARGB - return ((color & 0xFFFFFF00) >>> 8) | ((color & 0x000000FF) << 24); - } else { - // ABGR to ARGB - return (color & 0xFF00FF00) | ((color & 0x00FF0000) >>> 16) | ((color & 0x000000FF) << 16); - } + return ColorARGB.fromABGR(ColorABGR.fromNativeByteOrder(color)); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialFinderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialFinderImpl.java index 44a5b97f83..f0df79faaf 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialFinderImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialFinderImpl.java @@ -44,25 +44,25 @@ public MaterialFinderImpl() { public MaterialFinder blendMode(BlendMode blendMode) { Objects.requireNonNull(blendMode, "BlendMode may not be null"); - bits = (bits & ~BLEND_MODE_MASK) | (blendMode.ordinal() << BLEND_MODE_BIT_OFFSET); + this.bits = (this.bits & ~BLEND_MODE_MASK) | (blendMode.ordinal() << BLEND_MODE_BIT_OFFSET); return this; } @Override public MaterialFinder disableColorIndex(boolean disable) { - bits = disable ? (bits | COLOR_DISABLE_FLAG) : (bits & ~COLOR_DISABLE_FLAG); + this.bits = disable ? (this.bits | COLOR_DISABLE_FLAG) : (this.bits & ~COLOR_DISABLE_FLAG); return this; } @Override public MaterialFinder emissive(boolean isEmissive) { - bits = isEmissive ? (bits | EMISSIVE_FLAG) : (bits & ~EMISSIVE_FLAG); + this.bits = isEmissive ? (this.bits | EMISSIVE_FLAG) : (this.bits & ~EMISSIVE_FLAG); return this; } @Override public MaterialFinder disableDiffuse(boolean disable) { - bits = disable ? (bits | DIFFUSE_FLAG) : (bits & ~DIFFUSE_FLAG); + this.bits = disable ? (this.bits | DIFFUSE_FLAG) : (this.bits & ~DIFFUSE_FLAG); return this; } @@ -70,7 +70,7 @@ public MaterialFinder disableDiffuse(boolean disable) { public MaterialFinder ambientOcclusion(TriState mode) { Objects.requireNonNull(mode, "ambient occlusion TriState may not be null"); - bits = (bits & ~AO_MASK) | (mode.ordinal() << AO_BIT_OFFSET); + this.bits = (this.bits & ~AO_MASK) | (mode.ordinal() << AO_BIT_OFFSET); return this; } @@ -78,7 +78,7 @@ public MaterialFinder ambientOcclusion(TriState mode) { public MaterialFinder glint(TriState mode) { Objects.requireNonNull(mode, "glint TriState may not be null"); - bits = (bits & ~GLINT_MASK) | (mode.ordinal() << GLINT_BIT_OFFSET); + this.bits = (this.bits & ~GLINT_MASK) | (mode.ordinal() << GLINT_BIT_OFFSET); return this; } @@ -86,24 +86,24 @@ public MaterialFinder glint(TriState mode) { public MaterialFinder shadeMode(ShadeMode mode) { Objects.requireNonNull(mode, "ShadeMode may not be null"); - bits = (bits & ~SHADE_MODE_MASK) | (mode.ordinal() << SHADE_MODE_BIT_OFFSET); + this.bits = (this.bits & ~SHADE_MODE_MASK) | (mode.ordinal() << SHADE_MODE_BIT_OFFSET); return this; } @Override public MaterialFinder copyFrom(MaterialView material) { - bits = ((MaterialViewImpl) material).bits; + this.bits = ((MaterialViewImpl) material).bits; return this; } @Override public MaterialFinder clear() { - bits = defaultBits; + this.bits = defaultBits; return this; } @Override public RenderMaterial find() { - return RenderMaterialImpl.byIndex(bits); + return RenderMaterialImpl.byIndex(this.bits); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialViewImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialViewImpl.java index e6a4ea1e2e..3cc1cce7c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialViewImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/MaterialViewImpl.java @@ -85,36 +85,36 @@ protected MaterialViewImpl(int bits) { @Override public BlendMode blendMode() { - return BLEND_MODES[(bits & BLEND_MODE_MASK) >>> BLEND_MODE_BIT_OFFSET]; + return BLEND_MODES[(this.bits & BLEND_MODE_MASK) >>> BLEND_MODE_BIT_OFFSET]; } @Override public boolean disableColorIndex() { - return (bits & COLOR_DISABLE_FLAG) != 0; + return (this.bits & COLOR_DISABLE_FLAG) != 0; } @Override public boolean emissive() { - return (bits & EMISSIVE_FLAG) != 0; + return (this.bits & EMISSIVE_FLAG) != 0; } @Override public boolean disableDiffuse() { - return (bits & DIFFUSE_FLAG) != 0; + return (this.bits & DIFFUSE_FLAG) != 0; } @Override public TriState ambientOcclusion() { - return TRI_STATES[(bits & AO_MASK) >>> AO_BIT_OFFSET]; + return TRI_STATES[(this.bits & AO_MASK) >>> AO_BIT_OFFSET]; } @Override public TriState glint() { - return TRI_STATES[(bits & GLINT_MASK) >>> GLINT_BIT_OFFSET]; + return TRI_STATES[(this.bits & GLINT_MASK) >>> GLINT_BIT_OFFSET]; } @Override public ShadeMode shadeMode() { - return SHADE_MODES[(bits & SHADE_MODE_MASK) >>> SHADE_MODE_BIT_OFFSET]; + return SHADE_MODES[(this.bits & SHADE_MODE_MASK) >>> SHADE_MODE_BIT_OFFSET]; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/RenderMaterialImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/RenderMaterialImpl.java index 91a202828a..df529670ca 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/RenderMaterialImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/material/RenderMaterialImpl.java @@ -36,7 +36,7 @@ private RenderMaterialImpl(int bits) { } public int index() { - return bits; + return this.bits; } public static RenderMaterialImpl byIndex(int index) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshBuilderImpl.java index 34e69c8b1f..b94a1e3d15 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshBuilderImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshBuilderImpl.java @@ -30,39 +30,39 @@ public class MeshBuilderImpl implements MeshBuilder { private int[] data = new int[256]; private int index = 0; - private int limit = data.length; + private int limit = this.data.length; private final Maker maker = new Maker(); public MeshBuilderImpl() { - ensureCapacity(EncodingFormat.TOTAL_STRIDE); - maker.data = data; - maker.baseIndex = index; - maker.clear(); + this.ensureCapacity(EncodingFormat.TOTAL_STRIDE); + this.maker.data = this.data; + this.maker.baseIndex = this.index; + this.maker.clear(); } protected void ensureCapacity(int stride) { - if (stride > limit - index) { - limit *= 2; - final int[] bigger = new int[limit]; - System.arraycopy(data, 0, bigger, 0, index); - data = bigger; - maker.data = data; + if (stride > this.limit - this.index) { + this.limit *= 2; + final int[] bigger = new int[this.limit]; + System.arraycopy(this.data, 0, bigger, 0, this.index); + this.data = bigger; + this.maker.data = this.data; } } @Override public QuadEmitter getEmitter() { - maker.clear(); - return maker; + this.maker.clear(); + return this.maker; } @Override public Mesh build() { - final int[] packed = new int[index]; - System.arraycopy(data, 0, packed, 0, index); - index = 0; - maker.baseIndex = index; - maker.clear(); + final int[] packed = new int[this.index]; + System.arraycopy(this.data, 0, packed, 0, this.index); + this.index = 0; + this.maker.baseIndex = this.index; + this.maker.clear(); return new MeshImpl(packed); } @@ -75,10 +75,10 @@ public Mesh build() { private class Maker extends MutableQuadViewImpl { @Override public void emitDirectly() { - computeGeometry(); - index += EncodingFormat.TOTAL_STRIDE; - ensureCapacity(EncodingFormat.TOTAL_STRIDE); - baseIndex = index; + this.computeGeometry(); + MeshBuilderImpl.this.index += EncodingFormat.TOTAL_STRIDE; + MeshBuilderImpl.this.ensureCapacity(EncodingFormat.TOTAL_STRIDE); + this.baseIndex = MeshBuilderImpl.this.index; } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshImpl.java index 60fa624c90..84f5679914 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MeshImpl.java @@ -38,7 +38,7 @@ public class MeshImpl implements Mesh { @Override public void forEach(Consumer consumer) { - forEach(consumer, cursorPool.get()); + this.forEach(consumer, this.cursorPool.get()); } /** @@ -47,7 +47,7 @@ public void forEach(Consumer consumer) { * Also means renderer can hold final references to quad buffers. */ void forEach(Consumer consumer, QuadViewImpl cursor) { - final int limit = data.length; + final int limit = this.data.length; int index = 0; cursor.data = this.data; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MutableQuadViewImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MutableQuadViewImpl.java index 28f51af336..3a50a5f762 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MutableQuadViewImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/MutableQuadViewImpl.java @@ -50,87 +50,87 @@ public abstract class MutableQuadViewImpl extends QuadViewImpl implements QuadEm @Nullable public TextureAtlasSprite cachedSprite() { - return cachedSprite; + return this.cachedSprite; } public void cachedSprite(@Nullable TextureAtlasSprite sprite) { - cachedSprite = sprite; + this.cachedSprite = sprite; } public TextureAtlasSprite sprite(SpriteFinder finder) { - TextureAtlasSprite sprite = cachedSprite; + TextureAtlasSprite sprite = this.cachedSprite; if (sprite == null) { - cachedSprite = sprite = finder.find(this); + this.cachedSprite = sprite = finder.find(this); } return sprite; } public void clear() { - System.arraycopy(EMPTY, 0, data, baseIndex, EncodingFormat.TOTAL_STRIDE); - isGeometryInvalid = true; - nominalFace = null; - normalFlags(0); - tag(0); - colorIndex(-1); - cullFace(null); - material(SodiumRenderer.STANDARD_MATERIAL); - cachedSprite(null); + System.arraycopy(EMPTY, 0, this.data, this.baseIndex, EncodingFormat.TOTAL_STRIDE); + this.isGeometryInvalid = true; + this.nominalFace = null; + this.normalFlags(0); + this.tag(0); + this.colorIndex(-1); + this.cullFace(null); + this.material(SodiumRenderer.STANDARD_MATERIAL); + this.cachedSprite(null); } @Override public void load() { super.load(); - cachedSprite(null); + this.cachedSprite(null); } @Override public MutableQuadViewImpl pos(int vertexIndex, float x, float y, float z) { - final int index = baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X; - data[index] = Float.floatToRawIntBits(x); - data[index + 1] = Float.floatToRawIntBits(y); - data[index + 2] = Float.floatToRawIntBits(z); - isGeometryInvalid = true; + final int index = this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X; + this.data[index] = Float.floatToRawIntBits(x); + this.data[index + 1] = Float.floatToRawIntBits(y); + this.data[index + 2] = Float.floatToRawIntBits(z); + this.isGeometryInvalid = true; return this; } @Override public MutableQuadViewImpl color(int vertexIndex, int color) { - data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_COLOR] = color; + this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_COLOR] = color; return this; } @Override public MutableQuadViewImpl uv(int vertexIndex, float u, float v) { - final int i = baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U; - data[i] = Float.floatToRawIntBits(u); - data[i + 1] = Float.floatToRawIntBits(v); - cachedSprite(null); + final int i = this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U; + this.data[i] = Float.floatToRawIntBits(u); + this.data[i + 1] = Float.floatToRawIntBits(v); + this.cachedSprite(null); return this; } @Override public MutableQuadViewImpl spriteBake(TextureAtlasSprite sprite, int bakeFlags) { TextureHelper.bakeSprite(this, sprite, bakeFlags); - cachedSprite(sprite); + this.cachedSprite(sprite); return this; } @Override public MutableQuadViewImpl lightmap(int vertexIndex, int lightmap) { - data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_LIGHTMAP] = lightmap; + this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_LIGHTMAP] = lightmap; return this; } protected void normalFlags(int flags) { - data[baseIndex + HEADER_BITS] = EncodingFormat.normalFlags(data[baseIndex + HEADER_BITS], flags); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.normalFlags(this.data[this.baseIndex + HEADER_BITS], flags); } @Override public MutableQuadViewImpl normal(int vertexIndex, float x, float y, float z) { - normalFlags(normalFlags() | (1 << vertexIndex)); - data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_NORMAL] = NormI8.pack(x, y, z); + this.normalFlags(this.normalFlags() | (1 << vertexIndex)); + this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_NORMAL] = NormI8.pack(x, y, z); return this; } @@ -142,27 +142,27 @@ public final void populateMissingNormals() { if (normalFlags == 0b1111) return; - final int packedFaceNormal = packedFaceNormal(); + final int packedFaceNormal = this.packedFaceNormal(); for (int v = 0; v < 4; v++) { if ((normalFlags & (1 << v)) == 0) { - data[baseIndex + v * VERTEX_STRIDE + VERTEX_NORMAL] = packedFaceNormal; + this.data[this.baseIndex + v * VERTEX_STRIDE + VERTEX_NORMAL] = packedFaceNormal; } } - normalFlags(0b1111); + this.normalFlags(0b1111); } @Override public final MutableQuadViewImpl cullFace(@Nullable Direction face) { - data[baseIndex + HEADER_BITS] = EncodingFormat.cullFace(data[baseIndex + HEADER_BITS], face); - nominalFace(face); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.cullFace(this.data[this.baseIndex + HEADER_BITS], face); + this.nominalFace(face); return this; } @Override public final MutableQuadViewImpl nominalFace(@Nullable Direction face) { - nominalFace = face; + this.nominalFace = face; return this; } @@ -172,19 +172,19 @@ public final MutableQuadViewImpl material(RenderMaterial material) { material = SodiumRenderer.STANDARD_MATERIAL; } - data[baseIndex + HEADER_BITS] = EncodingFormat.material(data[baseIndex + HEADER_BITS], (RenderMaterialImpl) material); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.material(this.data[this.baseIndex + HEADER_BITS], (RenderMaterialImpl) material); return this; } @Override public final MutableQuadViewImpl colorIndex(int colorIndex) { - data[baseIndex + HEADER_COLOR_INDEX] = colorIndex; + this.data[this.baseIndex + HEADER_COLOR_INDEX] = colorIndex; return this; } @Override public final MutableQuadViewImpl tag(int tag) { - data[baseIndex + HEADER_TAG] = tag; + this.data[this.baseIndex + HEADER_TAG] = tag; return this; } @@ -193,15 +193,15 @@ public MutableQuadViewImpl copyFrom(QuadView quad) { final QuadViewImpl q = (QuadViewImpl) quad; q.computeGeometry(); - System.arraycopy(q.data, q.baseIndex, data, baseIndex, EncodingFormat.TOTAL_STRIDE); - faceNormal.set(q.faceNormal); - nominalFace = q.nominalFace; - isGeometryInvalid = false; + System.arraycopy(q.data, q.baseIndex, this.data, this.baseIndex, EncodingFormat.TOTAL_STRIDE); + this.faceNormal.set(q.faceNormal); + this.nominalFace = q.nominalFace; + this.isGeometryInvalid = false; if (quad instanceof MutableQuadViewImpl mutableQuad) { - cachedSprite(mutableQuad.cachedSprite()); + this.cachedSprite(mutableQuad.cachedSprite()); } else { - cachedSprite(null); + this.cachedSprite(null); } return this; @@ -213,30 +213,30 @@ public MutableQuadViewImpl copyFrom(QuadView quad) { * Only use this if you are also setting the geometry and sprite. */ private void fromVanillaInternal(int[] quadData, int startIndex) { - System.arraycopy(quadData, startIndex, data, baseIndex + HEADER_STRIDE, QuadView.VANILLA_QUAD_STRIDE); + System.arraycopy(quadData, startIndex, this.data, this.baseIndex + HEADER_STRIDE, QuadView.VANILLA_QUAD_STRIDE); - int colorIndex = baseIndex + VERTEX_COLOR; + int colorIndex = this.baseIndex + VERTEX_COLOR; for (int i = 0; i < 4; i++) { - data[colorIndex] = ColorHelper.fromVanillaColor(data[colorIndex]); + this.data[colorIndex] = ColorHelper.fromVanillaColor(this.data[colorIndex]); colorIndex += VERTEX_STRIDE; } } @Override public final MutableQuadViewImpl fromVanilla(int[] quadData, int startIndex) { - fromVanillaInternal(quadData, startIndex); - isGeometryInvalid = true; - cachedSprite(null); + this.fromVanillaInternal(quadData, startIndex); + this.isGeometryInvalid = true; + this.cachedSprite(null); return this; } @Override public final MutableQuadViewImpl fromVanilla(BakedQuad quad, RenderMaterial material, @Nullable Direction cullFace) { - fromVanillaInternal(quad.getVertices(), 0); - data[baseIndex + HEADER_BITS] = EncodingFormat.cullFace(0, cullFace); - nominalFace(quad.getDirection()); - colorIndex(quad.getTintIndex()); + this.fromVanillaInternal(quad.getVertices(), 0); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.cullFace(0, cullFace); + this.nominalFace(quad.getDirection()); + this.colorIndex(quad.getTintIndex()); // TODO: Is this the same as hasShade? if (!((BakedQuadView) quad).hasShade()) { @@ -247,19 +247,19 @@ public final MutableQuadViewImpl fromVanilla(BakedQuad quad, RenderMaterial mate material = RenderMaterialImpl.setAmbientOcclusion((RenderMaterialImpl) material, TriState.FALSE); } - material(material); - tag(0); + this.material(material); + this.tag(0); // Copy geometry cached inside the quad BakedQuadView bakedView = (BakedQuadView) quad; - NormI8.unpack(bakedView.getFaceNormal(), faceNormal); - data[baseIndex + HEADER_FACE_NORMAL] = bakedView.getFaceNormal(); - int headerBits = EncodingFormat.lightFace(data[baseIndex + HEADER_BITS], bakedView.getLightFace()); + NormI8.unpack(bakedView.getFaceNormal(), this.faceNormal); + this.data[this.baseIndex + HEADER_FACE_NORMAL] = bakedView.getFaceNormal(); + int headerBits = EncodingFormat.lightFace(this.data[this.baseIndex + HEADER_BITS], bakedView.getLightFace()); headerBits = EncodingFormat.normalFace(headerBits, bakedView.getNormalFace()); - data[baseIndex + HEADER_BITS] = EncodingFormat.geometryFlags(headerBits, bakedView.getFlags()); - isGeometryInvalid = false; + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.geometryFlags(headerBits, bakedView.getFlags()); + this.isGeometryInvalid = false; - cachedSprite(quad.getSprite()); + this.cachedSprite(quad.getSprite()); return this; } @@ -271,8 +271,8 @@ public final MutableQuadViewImpl fromVanilla(BakedQuad quad, RenderMaterial mate @Override public final MutableQuadViewImpl emit() { - emitDirectly(); - clear(); + this.emitDirectly(); + this.clear(); return this; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/QuadViewImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/QuadViewImpl.java index fce995a97b..21daee8872 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/QuadViewImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/mesh/QuadViewImpl.java @@ -49,7 +49,7 @@ public class QuadViewImpl implements QuadView, ModelQuadView { /** Size and where it comes from will vary in subtypes. But in all cases quad is fully encoded to array. */ protected int[] data; - /** Beginning of the quad. Also the header index. */ + /** Beginning of the quad. Also, the header index. */ protected int baseIndex = 0; /** @@ -57,59 +57,59 @@ public class QuadViewImpl implements QuadView, ModelQuadView { * The encoded data must contain valid computed geometry. */ public void load() { - isGeometryInvalid = false; - nominalFace = lightFace(); - NormI8.unpack(packedFaceNormal(), faceNormal); + this.isGeometryInvalid = false; + this.nominalFace = this.lightFace(); + NormI8.unpack(this.packedFaceNormal(), this.faceNormal); } protected void computeGeometry() { - if (isGeometryInvalid) { - isGeometryInvalid = false; + if (this.isGeometryInvalid) { + this.isGeometryInvalid = false; - NormalHelper.computeFaceNormal(faceNormal, this); - int packedFaceNormal = NormI8.pack(faceNormal); - data[baseIndex + HEADER_FACE_NORMAL] = packedFaceNormal; + NormalHelper.computeFaceNormal(this.faceNormal, this); + int packedFaceNormal = NormI8.pack(this.faceNormal); + this.data[this.baseIndex + HEADER_FACE_NORMAL] = packedFaceNormal; // depends on face normal Direction lightFace = GeometryHelper.lightFace(this); - data[baseIndex + HEADER_BITS] = EncodingFormat.lightFace(data[baseIndex + HEADER_BITS], lightFace); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.lightFace(this.data[this.baseIndex + HEADER_BITS], lightFace); // depends on face normal - data[baseIndex + HEADER_BITS] = EncodingFormat.normalFace(data[baseIndex + HEADER_BITS], ModelQuadFacing.fromPackedNormal(packedFaceNormal)); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.normalFace(this.data[this.baseIndex + HEADER_BITS], ModelQuadFacing.fromPackedNormal(packedFaceNormal)); // depends on light face - data[baseIndex + HEADER_BITS] = EncodingFormat.geometryFlags(data[baseIndex + HEADER_BITS], ModelQuadFlags.getQuadFlags(this, lightFace)); + this.data[this.baseIndex + HEADER_BITS] = EncodingFormat.geometryFlags(this.data[this.baseIndex + HEADER_BITS], ModelQuadFlags.getQuadFlags(this, lightFace)); } } /** gets flags used for lighting - lazily computed via {@link ModelQuadFlags#getQuadFlags}. */ public int geometryFlags() { - computeGeometry(); - return EncodingFormat.geometryFlags(data[baseIndex + HEADER_BITS]); + this.computeGeometry(); + return EncodingFormat.geometryFlags(this.data[this.baseIndex + HEADER_BITS]); } public boolean hasShade() { - return !material().disableDiffuse(); + return !this.material().disableDiffuse(); } @Override public float x(int vertexIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X]); } @Override public float y(int vertexIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_Y]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_Y]); } @Override public float z(int vertexIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_Z]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_Z]); } @Override public float posByIndex(int vertexIndex, int coordinateIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X + coordinateIndex]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X + coordinateIndex]); } @Override @@ -118,24 +118,24 @@ public Vector3f copyPos(int vertexIndex, @Nullable Vector3f target) { target = new Vector3f(); } - final int index = baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X; - target.set(Float.intBitsToFloat(data[index]), Float.intBitsToFloat(data[index + 1]), Float.intBitsToFloat(data[index + 2])); + final int index = this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_X; + target.set(Float.intBitsToFloat(this.data[index]), Float.intBitsToFloat(this.data[index + 1]), Float.intBitsToFloat(this.data[index + 2])); return target; } @Override public int color(int vertexIndex) { - return data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_COLOR]; + return this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_COLOR]; } @Override public float u(int vertexIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U]); } @Override public float v(int vertexIndex) { - return Float.intBitsToFloat(data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_V]); + return Float.intBitsToFloat(this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_V]); } @Override @@ -144,70 +144,70 @@ public Vector2f copyUv(int vertexIndex, @Nullable Vector2f target) { target = new Vector2f(); } - final int index = baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U; - target.set(Float.intBitsToFloat(data[index]), Float.intBitsToFloat(data[index + 1])); + final int index = this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_U; + target.set(Float.intBitsToFloat(this.data[index]), Float.intBitsToFloat(this.data[index + 1])); return target; } @Override public int lightmap(int vertexIndex) { - return data[baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_LIGHTMAP]; + return this.data[this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_LIGHTMAP]; } public int normalFlags() { - return EncodingFormat.normalFlags(data[baseIndex + HEADER_BITS]); + return EncodingFormat.normalFlags(this.data[this.baseIndex + HEADER_BITS]); } @Override public boolean hasNormal(int vertexIndex) { - return (normalFlags() & (1 << vertexIndex)) != 0; + return (this.normalFlags() & (1 << vertexIndex)) != 0; } /** True if any vertex normal has been set. */ public boolean hasVertexNormals() { - return normalFlags() != 0; + return this.normalFlags() != 0; } /** True if all vertex normals have been set. */ public boolean hasAllVertexNormals() { - return (normalFlags() & 0b1111) == 0b1111; + return (this.normalFlags() & 0b1111) == 0b1111; } protected final int normalIndex(int vertexIndex) { - return baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_NORMAL; + return this.baseIndex + vertexIndex * VERTEX_STRIDE + VERTEX_NORMAL; } /** * This method will only return a meaningful value if {@link #hasNormal} returns {@code true} for the same vertex index. */ public int packedNormal(int vertexIndex) { - return data[normalIndex(vertexIndex)]; + return this.data[this.normalIndex(vertexIndex)]; } @Override public float normalX(int vertexIndex) { - return hasNormal(vertexIndex) ? NormI8.unpackX(data[normalIndex(vertexIndex)]) : Float.NaN; + return this.hasNormal(vertexIndex) ? NormI8.unpackX(this.data[this.normalIndex(vertexIndex)]) : Float.NaN; } @Override public float normalY(int vertexIndex) { - return hasNormal(vertexIndex) ? NormI8.unpackY(data[normalIndex(vertexIndex)]) : Float.NaN; + return this.hasNormal(vertexIndex) ? NormI8.unpackY(this.data[this.normalIndex(vertexIndex)]) : Float.NaN; } @Override public float normalZ(int vertexIndex) { - return hasNormal(vertexIndex) ? NormI8.unpackZ(data[normalIndex(vertexIndex)]) : Float.NaN; + return this.hasNormal(vertexIndex) ? NormI8.unpackZ(this.data[this.normalIndex(vertexIndex)]) : Float.NaN; } @Override @Nullable public Vector3f copyNormal(int vertexIndex, @Nullable Vector3f target) { - if (hasNormal(vertexIndex)) { + if (this.hasNormal(vertexIndex)) { if (target == null) { target = new Vector3f(); } - final int normal = data[normalIndex(vertexIndex)]; + final int normal = this.data[this.normalIndex(vertexIndex)]; NormI8.unpack(normal, target); return target; } else { @@ -218,56 +218,56 @@ public Vector3f copyNormal(int vertexIndex, @Nullable Vector3f target) { @Override @Nullable public final Direction cullFace() { - return EncodingFormat.cullFace(data[baseIndex + HEADER_BITS]); + return EncodingFormat.cullFace(this.data[this.baseIndex + HEADER_BITS]); } @Override @NotNull public final Direction lightFace() { - computeGeometry(); - return EncodingFormat.lightFace(data[baseIndex + HEADER_BITS]); + this.computeGeometry(); + return EncodingFormat.lightFace(this.data[this.baseIndex + HEADER_BITS]); } public final ModelQuadFacing normalFace() { - computeGeometry(); - return EncodingFormat.normalFace(data[baseIndex + HEADER_BITS]); + this.computeGeometry(); + return EncodingFormat.normalFace(this.data[this.baseIndex + HEADER_BITS]); } @Override @Nullable public final Direction nominalFace() { - return nominalFace; + return this.nominalFace; } public final int packedFaceNormal() { - computeGeometry(); - return data[baseIndex + HEADER_FACE_NORMAL]; + this.computeGeometry(); + return this.data[this.baseIndex + HEADER_FACE_NORMAL]; } @Override public final Vector3f faceNormal() { - computeGeometry(); - return faceNormal; + this.computeGeometry(); + return this.faceNormal; } @Override public final RenderMaterialImpl material() { - return EncodingFormat.material(data[baseIndex + HEADER_BITS]); + return EncodingFormat.material(this.data[this.baseIndex + HEADER_BITS]); } @Override public final int colorIndex() { - return data[baseIndex + HEADER_COLOR_INDEX]; + return this.data[this.baseIndex + HEADER_COLOR_INDEX]; } @Override public final int tag() { - return data[baseIndex + HEADER_TAG]; + return this.data[this.baseIndex + HEADER_TAG]; } @Override public final void toVanilla(int[] target, int targetIndex) { - System.arraycopy(data, baseIndex + HEADER_STRIDE, target, targetIndex, QUAD_STRIDE); + System.arraycopy(this.data, this.baseIndex + HEADER_STRIDE, target, targetIndex, QUAD_STRIDE); // The color is the fourth integer in each vertex. // EncodingFormat.VERTEX_COLOR is not used because it also @@ -284,52 +284,52 @@ public final void toVanilla(int[] target, int targetIndex) { @Override public float getX(int idx) { - return x(idx); + return this.x(idx); } @Override public float getY(int idx) { - return y(idx); + return this.y(idx); } @Override public float getZ(int idx) { - return z(idx); + return this.z(idx); } @Override public int getColor(int idx) { - return ColorHelper.toVanillaColor(color(idx)); + return this.color(idx); } @Override public float getTexU(int idx) { - return u(idx); + return this.u(idx); } @Override public float getTexV(int idx) { - return v(idx); + return this.v(idx); } @Override public int getVertexNormal(int idx) { - return data[normalIndex(idx)]; + return this.data[this.normalIndex(idx)]; } @Override public int getFaceNormal() { - return packedFaceNormal(); + return this.packedFaceNormal(); } @Override public int getLight(int idx) { - return lightmap(idx); + return this.lightmap(idx); } @Override public int getColorIndex() { - return material().disableColorIndex() ? -1 : colorIndex(); + return this.material().disableColorIndex() ? -1 : this.colorIndex(); } @Override @@ -339,11 +339,11 @@ public TextureAtlasSprite getSprite() { @Override public Direction getLightFace() { - return lightFace(); + return this.lightFace(); } @Override public int getFlags() { - return geometryFlags(); + return this.geometryFlags(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java index 828e2663ae..90a4448829 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractBlockRenderContext.java @@ -32,6 +32,8 @@ import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; import java.util.function.Supplier; @@ -58,18 +60,19 @@ public abstract class AbstractBlockRenderContext extends AbstractRenderContext { STANDARD_MATERIALS[i] = SodiumRenderer.INSTANCE.materialFinder().ambientOcclusion(state).find(); } } + private final MutableQuadViewImpl editorQuad = new MutableQuadViewImpl() { { - data = new int[EncodingFormat.TOTAL_STRIDE]; - clear(); + this.data = new int[EncodingFormat.TOTAL_STRIDE]; + this.clear(); } @Override public void emitDirectly() { - if (type == null) { + if (AbstractBlockRenderContext.this.type == null) { throw new IllegalStateException("No render type is set but an FRAPI object was asked to render!"); } - renderQuad(this); + AbstractBlockRenderContext.this.renderQuad(this); } }; @@ -103,6 +106,18 @@ public void emitDirectly() { */ protected SodiumModelData modelData; + /** + * Submodel ModelData stack pushed by FFAPI's MultipartBakedModelMixin during multipart traversal. + * See this commit. + */ + protected final Deque modelDataStack = new ArrayDeque<>(); + + /** + * Submodel AO override pushed by FFAPI's MultipartBakedModelMixin; non-DEFAULT forces the parent multipart's choice. + * See this commit. + */ + protected TriState useAO = TriState.DEFAULT; + private final BlockOcclusionCache occlusionCache = new BlockOcclusionCache(); private boolean enableCulling = true; // Cull cache (as it's checked per-quad instead of once per side like in vanilla) @@ -112,8 +127,8 @@ public void emitDirectly() { protected RandomSource random; protected long randomSeed; protected final Supplier randomSupplier = () -> { - random.setSeed(randomSeed); - return random; + this.random.setSeed(this.randomSeed); + return this.random; }; /** @@ -158,7 +173,19 @@ public ItemDisplayContext itemTransformationMode() { throw new UnsupportedOperationException("itemTransformationMode can only be called on an item render context."); } - @SuppressWarnings("removal") + protected SodiumModelData currentModelData() { + SodiumModelData top = this.modelDataStack.peek(); + return top != null ? top : this.modelData; + } + + private static AmbientOcclusionMode applyAOOverride(AmbientOcclusionMode mode, TriState override) { + return switch (override) { + case TRUE -> AmbientOcclusionMode.ENABLED; + case FALSE -> AmbientOcclusionMode.DISABLED; + default -> mode; + }; + } + @Deprecated @Override public BakedModelConsumer bakedModelConsumer() { @@ -195,7 +222,7 @@ protected void prepareCulling(boolean enableCulling) { protected void prepareAoInfo(boolean modelAo) { this.useAmbientOcclusion = Minecraft.useAmbientOcclusion(); // Ignore the incorrect IDEA warning here. - this.defaultLightMode = this.useAmbientOcclusion && modelAo && PlatformBlockAccess.getInstance().getLightEmission(state, level, pos) == 0 ? LightMode.SMOOTH : LightMode.FLAT; + this.defaultLightMode = this.useAmbientOcclusion && modelAo && PlatformBlockAccess.getInstance().getLightEmission(this.state, this.level, this.pos) == 0 ? LightMode.SMOOTH : LightMode.FLAT; } protected void shadeQuad(MutableQuadViewImpl quad, LightMode lightMode, boolean emissive, ShadeMode shadeMode) { @@ -220,6 +247,9 @@ protected void shadeQuad(MutableQuadViewImpl quad, LightMode lightMode, boolean public void bufferDefaultModel(BakedModel model, @Nullable BlockState state) { MutableQuadViewImpl editorQuad = this.editorQuad; + // Per-submodel state pushed by FFAPI's multipart mixin, falls through to the root context otherwise. + final SodiumModelData currentData = this.currentModelData(); + final TriState aoOverride = this.useAO; // If there is no transform, we can check the culling face once for all the quads, // and we don't need to check for transforms per-quad. @@ -229,15 +259,17 @@ public void bufferDefaultModel(BakedModel model, @Nullable BlockState state) { final Direction cullFace = ModelHelper.faceFromIndex(i); RandomSource random = this.randomSupplier.get(); - AmbientOcclusionMode ao = PlatformBlockAccess.getInstance().usesAmbientOcclusion(model, state, modelData, type, slice, pos); + AmbientOcclusionMode ao = applyAOOverride( + PlatformBlockAccess.getInstance().usesAmbientOcclusion(model, state, currentData, this.type, this.slice, this.pos), + aoOverride); if (noTransform) { if (!this.isFaceCulled(cullFace)) { - final List quads = PlatformModelAccess.getInstance().getQuads(level, pos, model, state, cullFace, random, type, modelData); + final List quads = PlatformModelAccess.getInstance().getQuads(this.level, this.pos, model, state, cullFace, random, this.type, currentData); final int count = quads.size(); for (int j = 0; j < count; j++) { final BakedQuad q = quads.get(j); - editorQuad.fromVanilla(q, (type == RenderType.tripwire() || type == RenderType.translucent()) ? TRANSLUCENT_MATERIAL : STANDARD_MATERIALS[ao.ordinal()], cullFace); + editorQuad.fromVanilla(q, (this.type == RenderType.tripwire() || this.type == RenderType.translucent()) ? TRANSLUCENT_MATERIAL : STANDARD_MATERIALS[ao.ordinal()], cullFace); // Call processQuad instead of emit for efficiency // (avoid unnecessarily clearing data, trying to apply transforms, and performing cull check again) @@ -245,12 +277,12 @@ public void bufferDefaultModel(BakedModel model, @Nullable BlockState state) { } } } else { - final List quads = PlatformModelAccess.getInstance().getQuads(level, pos, model, state, cullFace, random, type, modelData); + final List quads = PlatformModelAccess.getInstance().getQuads(this.level, this.pos, model, state, cullFace, random, this.type, currentData); final int count = quads.size(); for (int j = 0; j < count; j++) { final BakedQuad q = quads.get(j); - editorQuad.fromVanilla(q, (type == RenderType.tripwire() || type == RenderType.translucent()) ? TRANSLUCENT_MATERIAL : STANDARD_MATERIALS[ao.ordinal()], cullFace); + editorQuad.fromVanilla(q, (this.type == RenderType.tripwire() || this.type == RenderType.translucent()) ? TRANSLUCENT_MATERIAL : STANDARD_MATERIALS[ao.ordinal()], cullFace); // Call renderQuad instead of emit for efficiency // (avoid unnecessarily clearing data) this.renderQuad(editorQuad); @@ -261,12 +293,11 @@ public void bufferDefaultModel(BakedModel model, @Nullable BlockState state) { editorQuad.clear(); } - @SuppressWarnings("removal") @Deprecated private class BakedModelConsumerImpl implements BakedModelConsumer { @Override public void accept(BakedModel model) { - accept(model, AbstractBlockRenderContext.this.state); + this.accept(model, AbstractBlockRenderContext.this.state); } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractRenderContext.java index 6074c497cf..1473970372 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/AbstractRenderContext.java @@ -29,10 +29,10 @@ public abstract class AbstractRenderContext implements RenderContext { private QuadTransform activeTransform = NO_TRANSFORM; private final ObjectArrayList transformStack = new ObjectArrayList<>(); private final QuadTransform stackTransform = q -> { - int i = transformStack.size() - 1; + int i = this.transformStack.size() - 1; while (i >= 0) { - if (!transformStack.get(i--).transform(q)) { + if (!this.transformStack.get(i--).transform(q)) { return false; } } @@ -44,12 +44,12 @@ public abstract class AbstractRenderContext implements RenderContext { private final Consumer meshConsumer = mesh -> mesh.outputTo(getEmitter()); protected final boolean transform(MutableQuadView q) { - return activeTransform.transform(q); + return this.activeTransform.transform(q); } @Override public boolean hasTransform() { - return activeTransform != NO_TRANSFORM; + return this.activeTransform != NO_TRANSFORM; } @Override @@ -58,23 +58,23 @@ public void pushTransform(QuadTransform transform) { throw new NullPointerException("Renderer received null QuadTransform."); } - transformStack.push(transform); + this.transformStack.push(transform); - if (transformStack.size() == 1) { - activeTransform = transform; - } else if (transformStack.size() == 2) { - activeTransform = stackTransform; + if (this.transformStack.size() == 1) { + this.activeTransform = transform; + } else if (this.transformStack.size() == 2) { + this.activeTransform = this.stackTransform; } } @Override public void popTransform() { - transformStack.pop(); + this.transformStack.pop(); - if (transformStack.isEmpty()) { - activeTransform = NO_TRANSFORM; - } else if (transformStack.size() == 1) { - activeTransform = transformStack.get(0); + if (this.transformStack.isEmpty()) { + this.activeTransform = NO_TRANSFORM; + } else if (this.transformStack.size() == 1) { + this.activeTransform = this.transformStack.get(0); } } @@ -82,6 +82,6 @@ public void popTransform() { @Deprecated @Override public Consumer meshConsumer() { - return meshConsumer; + return this.meshConsumer; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/ItemRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/ItemRenderContext.java index 62dfae8cad..cd82833739 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/ItemRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/ItemRenderContext.java @@ -19,11 +19,12 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.MatrixUtil; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.caffeinemc.mods.sodium.client.render.frapi.mesh.EncodingFormat; import net.caffeinemc.mods.sodium.client.render.frapi.mesh.MutableQuadViewImpl; import net.caffeinemc.mods.sodium.client.render.texture.SpriteFinderCache; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.mixin.features.render.frapi.ItemRendererAccessor; import net.fabricmc.fabric.api.renderer.v1.material.BlendMode; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; @@ -59,13 +60,13 @@ public class ItemRenderContext extends AbstractRenderContext { private final MutableQuadViewImpl editorQuad = new MutableQuadViewImpl() { { - data = new int[EncodingFormat.TOTAL_STRIDE]; - clear(); + this.data = new int[EncodingFormat.TOTAL_STRIDE]; + this.clear(); } @Override public void emitDirectly() { - renderQuad(this); + ItemRenderContext.this.renderQuad(this); } }; @@ -77,8 +78,8 @@ public void emitDirectly() { private final RandomSource random = new SingleThreadedRandomSource(ITEM_RANDOM_SEED); private final Supplier randomSupplier = () -> { - random.setSeed(ITEM_RANDOM_SEED); - return random; + this.random.setSeed(ITEM_RANDOM_SEED); + return this.random; }; private ItemStack itemStack; @@ -110,8 +111,8 @@ public ItemRenderContext(ItemColors colorMap, VanillaModelBufferer vanillaBuffer @Override public QuadEmitter getEmitter() { - editorQuad.clear(); - return editorQuad; + this.editorQuad.clear(); + return this.editorQuad; } @Override @@ -121,88 +122,87 @@ public boolean isFaceCulled(@Nullable Direction face) { @Override public ItemDisplayContext itemTransformationMode() { - return transformMode; + return this.transformMode; } - @SuppressWarnings("removal") @Deprecated @Override public BakedModelConsumer bakedModelConsumer() { - return vanillaModelConsumer; + return this.vanillaModelConsumer; } public void renderModel(ItemStack itemStack, ItemDisplayContext transformMode, boolean invert, PoseStack poseStack, MultiBufferSource bufferSource, int lightmap, int overlay, BakedModel model) { this.itemStack = itemStack; this.transformMode = transformMode; this.poseStack = poseStack; - matPosition = poseStack.last().pose(); - trustedNormals = poseStack.last().trustedNormals; - matNormal = poseStack.last().normal(); + this.matPosition = poseStack.last().pose(); + this.trustedNormals = poseStack.last().trustedNormals; + this.matNormal = poseStack.last().normal(); this.bufferSource = bufferSource; this.lightmap = lightmap; this.overlay = overlay; - computeOutputInfo(); + this.computeOutputInfo(); - ((FabricBakedModel) model).emitItemQuads(itemStack, randomSupplier, this); + ((FabricBakedModel) model).emitItemQuads(itemStack, this.randomSupplier, this); this.itemStack = null; this.poseStack = null; this.bufferSource = null; - dynamicDisplayGlintEntry = null; - translucentVertexConsumer = null; - cutoutVertexConsumer = null; - translucentGlintVertexConsumer = null; - cutoutGlintVertexConsumer = null; - defaultVertexConsumer = null; + this.dynamicDisplayGlintEntry = null; + this.translucentVertexConsumer = null; + this.cutoutVertexConsumer = null; + this.translucentGlintVertexConsumer = null; + this.cutoutGlintVertexConsumer = null; + this.defaultVertexConsumer = null; } private void computeOutputInfo() { - isDefaultTranslucent = true; - isTranslucentDirect = true; + this.isDefaultTranslucent = true; + this.isTranslucentDirect = true; - Item item = itemStack.getItem(); + Item item = this.itemStack.getItem(); if (item instanceof BlockItem blockItem) { BlockState state = blockItem.getBlock().defaultBlockState(); RenderType renderType = ItemBlockRenderTypes.getChunkRenderType(state); if (renderType != RenderType.translucent()) { - isDefaultTranslucent = false; + this.isDefaultTranslucent = false; } - if (transformMode != ItemDisplayContext.GUI && !transformMode.firstPerson()) { - isTranslucentDirect = false; + if (this.transformMode != ItemDisplayContext.GUI && !this.transformMode.firstPerson()) { + this.isTranslucentDirect = false; } } - isDefaultGlint = itemStack.hasFoil(); - isGlintDynamicDisplay = ItemRendererAccessor.sodium$hasAnimatedTexture(itemStack); + this.isDefaultGlint = this.itemStack.hasFoil(); + this.isGlintDynamicDisplay = ItemRendererAccessor.sodium$hasAnimatedTexture(this.itemStack); - defaultVertexConsumer = getVertexConsumer(BlendMode.DEFAULT, TriState.DEFAULT); + this.defaultVertexConsumer = this.getVertexConsumer(BlendMode.DEFAULT, TriState.DEFAULT); } private void renderQuad(MutableQuadViewImpl quad) { - if (!transform(quad)) { + if (!this.transform(quad)) { return; } final RenderMaterial mat = quad.material(); final int colorIndex = mat.disableColorIndex() ? -1 : quad.colorIndex(); final boolean emissive = mat.emissive(); - final VertexConsumer vertexConsumer = getVertexConsumer(mat.blendMode(), mat.glint()); + final VertexConsumer vertexConsumer = this.getVertexConsumer(mat.blendMode(), mat.glint()); - colorizeQuad(quad, colorIndex); - shadeQuad(quad, emissive); - bufferQuad(quad, vertexConsumer); + this.colorizeQuad(quad, colorIndex); + this.shadeQuad(quad, emissive); + this.bufferQuad(quad, vertexConsumer); } private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { if (colorIndex != -1) { - final int itemColor = colorMap.getColor(itemStack, colorIndex); + final int itemColor = this.colorMap.getColor(this.itemStack, colorIndex); for (int i = 0; i < 4; i++) { - quad.color(i, ColorHelper.multiplyColor(itemColor, quad.color(i))); + quad.color(i, ColorMixer.mulComponentWise(itemColor, quad.color(i))); } } } @@ -222,8 +222,11 @@ private void shadeQuad(MutableQuadViewImpl quad, boolean emissive) { } private void bufferQuad(MutableQuadViewImpl quad, VertexConsumer vertexConsumer) { - QuadEncoder.writeQuadVertices(quad, vertexConsumer, overlay, matPosition, trustedNormals, matNormal); - SpriteUtil.markSpriteActive(quad.sprite(SpriteFinderCache.forBlockAtlas())); + QuadEncoder.writeQuadVertices(quad, vertexConsumer, this.overlay, this.matPosition, this.trustedNormals, this.matNormal); + var sprite = quad.sprite(SpriteFinderCache.forBlockAtlas()); + if (sprite != null) { + SpriteUtil.INSTANCE.markSpriteActive(sprite); + } } /** @@ -236,103 +239,102 @@ private VertexConsumer getVertexConsumer(BlendMode blendMode, TriState glintMode boolean glint; if (blendMode == BlendMode.DEFAULT) { - translucent = isDefaultTranslucent; + translucent = this.isDefaultTranslucent; } else { translucent = blendMode == BlendMode.TRANSLUCENT; } if (glintMode == TriState.DEFAULT) { - glint = isDefaultGlint; + glint = this.isDefaultGlint; } else { glint = glintMode == TriState.TRUE; } if (translucent) { if (glint) { - if (translucentGlintVertexConsumer == null) { - translucentGlintVertexConsumer = createTranslucentVertexConsumer(true); + if (this.translucentGlintVertexConsumer == null) { + this.translucentGlintVertexConsumer = this.createTranslucentVertexConsumer(true); } - return translucentGlintVertexConsumer; + return this.translucentGlintVertexConsumer; } else { - if (translucentVertexConsumer == null) { - translucentVertexConsumer = createTranslucentVertexConsumer(false); + if (this.translucentVertexConsumer == null) { + this.translucentVertexConsumer = this.createTranslucentVertexConsumer(false); } - return translucentVertexConsumer; + return this.translucentVertexConsumer; } } else { if (glint) { - if (cutoutGlintVertexConsumer == null) { - cutoutGlintVertexConsumer = createCutoutVertexConsumer(true); + if (this.cutoutGlintVertexConsumer == null) { + this.cutoutGlintVertexConsumer = this.createCutoutVertexConsumer(true); } - return cutoutGlintVertexConsumer; + return this.cutoutGlintVertexConsumer; } else { - if (cutoutVertexConsumer == null) { - cutoutVertexConsumer = createCutoutVertexConsumer(false); + if (this.cutoutVertexConsumer == null) { + this.cutoutVertexConsumer = this.createCutoutVertexConsumer(false); } - return cutoutVertexConsumer; + return this.cutoutVertexConsumer; } } } private VertexConsumer createTranslucentVertexConsumer(boolean glint) { - if (glint && isGlintDynamicDisplay) { - return createDynamicDisplayGlintVertexConsumer(Minecraft.useShaderTransparency() && !isTranslucentDirect ? Sheets.translucentItemSheet() : Sheets.translucentCullBlockSheet()); + if (glint && this.isGlintDynamicDisplay) { + return this.createDynamicDisplayGlintVertexConsumer(Minecraft.useShaderTransparency() && !this.isTranslucentDirect ? Sheets.translucentItemSheet() : Sheets.translucentCullBlockSheet()); } - if (isTranslucentDirect) { - return ItemRenderer.getFoilBufferDirect(bufferSource, Sheets.translucentCullBlockSheet(), true, glint); + if (this.isTranslucentDirect) { + return ItemRenderer.getFoilBufferDirect(this.bufferSource, Sheets.translucentCullBlockSheet(), true, glint); } else if (Minecraft.useShaderTransparency()) { - return ItemRenderer.getFoilBuffer(bufferSource, Sheets.translucentItemSheet(), true, glint); + return ItemRenderer.getFoilBuffer(this.bufferSource, Sheets.translucentItemSheet(), true, glint); } else { - return ItemRenderer.getFoilBuffer(bufferSource, Sheets.translucentItemSheet(), true, glint); + return ItemRenderer.getFoilBuffer(this.bufferSource, Sheets.translucentItemSheet(), true, glint); } } private VertexConsumer createCutoutVertexConsumer(boolean glint) { - if (glint && isGlintDynamicDisplay) { - return createDynamicDisplayGlintVertexConsumer(Sheets.cutoutBlockSheet()); + if (glint && this.isGlintDynamicDisplay) { + return this.createDynamicDisplayGlintVertexConsumer(Sheets.cutoutBlockSheet()); } - return ItemRenderer.getFoilBufferDirect(bufferSource, Sheets.cutoutBlockSheet(), true, glint); + return ItemRenderer.getFoilBufferDirect(this.bufferSource, Sheets.cutoutBlockSheet(), true, glint); } private VertexConsumer createDynamicDisplayGlintVertexConsumer(RenderType type) { - if (dynamicDisplayGlintEntry == null) { - dynamicDisplayGlintEntry = poseStack.last().copy(); + if (this.dynamicDisplayGlintEntry == null) { + this.dynamicDisplayGlintEntry = this.poseStack.last().copy(); - if (transformMode == ItemDisplayContext.GUI) { - MatrixUtil.mulComponentWise(dynamicDisplayGlintEntry.pose(), 0.5F); - } else if (transformMode.firstPerson()) { - MatrixUtil.mulComponentWise(dynamicDisplayGlintEntry.pose(), 0.75F); + if (this.transformMode == ItemDisplayContext.GUI) { + MatrixUtil.mulComponentWise(this.dynamicDisplayGlintEntry.pose(), 0.5F); + } else if (this.transformMode.firstPerson()) { + MatrixUtil.mulComponentWise(this.dynamicDisplayGlintEntry.pose(), 0.75F); } } - return ItemRenderer.getCompassFoilBuffer(bufferSource, type, dynamicDisplayGlintEntry); + return ItemRenderer.getCompassFoilBuffer(this.bufferSource, type, this.dynamicDisplayGlintEntry); } public void bufferDefaultModel(BakedModel model, @Nullable BlockState state) { - if (hasTransform() || vanillaBufferer == null) { - VanillaModelEncoder.emitItemQuads(model, state, randomSupplier, ItemRenderContext.this); + if (this.hasTransform() || this.vanillaBufferer == null) { + VanillaModelEncoder.emitItemQuads(model, state, this.randomSupplier, this); } else { - vanillaBufferer.accept(model, itemStack, lightmap, overlay, poseStack, defaultVertexConsumer); + this.vanillaBufferer.accept(model, this.itemStack, this.lightmap, this.overlay, this.poseStack, this.defaultVertexConsumer); } } - @SuppressWarnings("removal") @Deprecated private class BakedModelConsumerImpl implements BakedModelConsumer { @Override public void accept(BakedModel model) { - accept(model, null); + this.accept(model, null); } @Override public void accept(BakedModel model, @Nullable BlockState state) { - bufferDefaultModel(model, state); + ItemRenderContext.this.bufferDefaultModel(model, state); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/NonTerrainBlockRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/NonTerrainBlockRenderContext.java index 130d240b2e..9741cdadce 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/NonTerrainBlockRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/NonTerrainBlockRenderContext.java @@ -18,13 +18,14 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.util.ColorARGB; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.model.light.LightMode; import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; import net.caffeinemc.mods.sodium.client.model.light.data.SingleBlockLightDataCache; -import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.caffeinemc.mods.sodium.client.render.frapi.mesh.MutableQuadViewImpl; import net.caffeinemc.mods.sodium.client.render.texture.SpriteFinderCache; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.services.SodiumModelData; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; import net.fabricmc.fabric.api.renderer.v1.material.ShadeMode; @@ -99,9 +100,9 @@ protected void processQuad(MutableQuadViewImpl quad) { } final boolean emissive = mat.emissive(); - colorizeQuad(quad, colorIndex); - shadeQuad(quad, lightMode, emissive, shadeMode); - bufferQuad(quad); + this.colorizeQuad(quad, colorIndex); + this.shadeQuad(quad, lightMode, emissive, shadeMode); + this.bufferQuad(quad); } private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { @@ -109,7 +110,7 @@ private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { final int blockColor = 0xFF000000 | this.colorMap.getColor(this.state, this.level, this.pos, colorIndex); for (int i = 0; i < 4; i++) { - quad.color(i, ColorHelper.multiplyColor(blockColor, quad.color(i))); + quad.color(i, ColorMixer.mulComponentWise(blockColor, quad.color(i))); } } } @@ -121,12 +122,15 @@ protected void shadeQuad(MutableQuadViewImpl quad, LightMode lightMode, boolean float[] brightnesses = this.quadLightData.br; for (int i = 0; i < 4; i++) { - quad.color(i, ColorHelper.multiplyRGB(quad.color(i), brightnesses[i])); + quad.color(i, ColorARGB.mulRGB(quad.color(i), brightnesses[i])); } } private void bufferQuad(MutableQuadViewImpl quad) { - QuadEncoder.writeQuadVertices(quad, vertexConsumer, overlay, matPosition, trustedNormals, matNormal); - SpriteUtil.markSpriteActive(quad.sprite(SpriteFinderCache.forBlockAtlas())); + QuadEncoder.writeQuadVertices(quad, this.vertexConsumer, this.overlay, this.matPosition, this.trustedNormals, this.matNormal); + var sprite = quad.sprite(SpriteFinderCache.forBlockAtlas()); + if (sprite != null) { + SpriteUtil.INSTANCE.markSpriteActive(sprite); + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/QuadEncoder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/QuadEncoder.java index 8925908a4b..fa38ca0f92 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/QuadEncoder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/frapi/render/QuadEncoder.java @@ -16,7 +16,7 @@ public class QuadEncoder { public static void writeQuadVertices(MutableQuadViewImpl quad, VertexConsumer vertexConsumer, int overlay, Matrix4f matPosition, boolean trustedNormals, Matrix3f matNormal) { - VertexBufferWriter writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + VertexBufferWriter writer = VertexConsumerUtils.convertOrLog(vertexConsumer, EntityVertex.FORMAT); if (writer != null) { writeQuadVertices(quad, writer, overlay, matPosition, trustedNormals, matNormal); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/CloudRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/CloudRenderer.java index e0e6cf6088..88c1403fd0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/CloudRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/CloudRenderer.java @@ -4,11 +4,11 @@ import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.*; -import net.caffeinemc.mods.sodium.api.vertex.format.common.ColorVertex; -import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.util.ColorARGB; -import net.caffeinemc.mods.sodium.api.util.ColorMixer; +import net.caffeinemc.mods.sodium.api.util.ColorU8; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.ColorVertex; import net.minecraft.client.Camera; import net.minecraft.client.CloudStatus; import net.minecraft.client.Minecraft; @@ -16,32 +16,50 @@ import net.minecraft.client.renderer.FogRenderer; import net.minecraft.client.renderer.ShaderInstance; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; +import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; -import org.joml.Vector3f; import org.lwjgl.opengl.GL32C; import org.lwjgl.system.MemoryStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.InputStream; import java.util.Objects; public class CloudRenderer { - private static final ResourceLocation CLOUDS_TEXTURE_ID = ResourceLocation.withDefaultNamespace("textures/environment/clouds.png"); + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-CloudRenderer"); - private CloudTextureData textureData; - private ShaderInstance shaderProgram; + private static final ResourceLocation CLOUDS_TEXTURE_ID = + ResourceLocation.withDefaultNamespace("textures/environment/clouds.png"); - private @Nullable CloudRenderer.CloudGeometry cachedGeometry; + private static final float CLOUD_HEIGHT = 4.0f; // The height of the cloud cells + private static final float CLOUD_WIDTH = 12.0f; // The width/length of cloud cells + + // Bitmasks for each cloud face + private static final int FACE_MASK_NEG_Y = 1 << 0; + private static final int FACE_MASK_POS_Y = 1 << 1; + private static final int FACE_MASK_NEG_X = 1 << 2; + private static final int FACE_MASK_POS_X = 1 << 3; + private static final int FACE_MASK_NEG_Z = 1 << 4; + private static final int FACE_MASK_POS_Z = 1 << 5; + + // The brightness of each fac + // The final color of each vertex is: vec4((texel.rgb * brightness), texel.a * 0.8) + private static final int BRIGHTNESS_POS_Y = ColorU8.normalizedFloatToByte(1.0F); // used for +Y + private static final int BRIGHTNESS_NEG_Y = ColorU8.normalizedFloatToByte(0.7F); // used for -Y + private static final int BRIGHTNESS_X_AXIS = ColorU8.normalizedFloatToByte(0.9F); // used for -X and +X + private static final int BRIGHTNESS_Z_AXIS = ColorU8.normalizedFloatToByte(0.8F); // used for -Z and +Z + + private @Nullable ShaderInstance shaderProgram; + private @Nullable CloudTextureData textureData; + private @Nullable CloudGeometry builtGeometry; public CloudRenderer(ResourceProvider resourceProvider) { - this.reloadTextures(resourceProvider); + this.reload(resourceProvider); } public void render(Camera camera, @@ -51,60 +69,78 @@ public void render(Camera camera, float ticks, float tickDelta) { - float cloudHeight = level.effects().getCloudHeight(); + float height = level.effects() + .getCloudHeight() + 0.33f; // arithmetic against NaN always produces NaN // Vanilla uses NaN height as a way to disable cloud rendering - if (Float.isNaN(cloudHeight)) { + if (Float.isNaN(height)) { return; } - // Skip rendering clouds if texture is completely blank - if (this.textureData.isBlank) { + // Skip rendering clouds if the texture data or shader program isn't available + // This can happen if the texture failed to load, or if the texture is completely empty + if (this.textureData == null || this.shaderProgram == null) { return; } - Vec3 pos = camera.getPosition(); + Vec3 cameraPos = camera.getPosition(); + int renderDistance = getCloudRenderDistance(); + var renderMode = Minecraft.getInstance().options.getCloudsType(); double cloudTime = (ticks + tickDelta) * 0.03F; - double cloudCenterX = (pos.x() + cloudTime); - double cloudCenterZ = (pos.z()) + 0.33D; - int cloudDistance = getCloudRenderDistance(); + // Translation of the clouds texture in world-space + double worldX = cameraPos.x() + cloudTime; + double worldZ = cameraPos.z() + 0.33D; - int centerCellX = (int) (Math.floor(cloudCenterX / 12.0)); - int centerCellZ = (int) (Math.floor(cloudCenterZ / 12.0)); + // The coordinates of the cloud cell which the camera is within + int cellX = Mth.floor(worldX / CLOUD_WIDTH); + int cellZ = Mth.floor(worldZ / CLOUD_WIDTH); - // -1 if below clouds, +1 if above clouds - var cloudType = Minecraft.getInstance().options.getCloudsType(); - int orientation = cloudType == CloudStatus.FANCY ? (int) Math.signum(pos.y() - cloudHeight) : 0; - var parameters = new CloudGeometryParameters(centerCellX, centerCellZ, cloudDistance, orientation, cloudType); + // The orientation of the camera relative to the clouds + // This is used to cull back-facing geometry + ViewOrientation orientation; - CloudGeometry geometry = this.cachedGeometry; + if (renderMode == CloudStatus.FANCY) { + orientation = ViewOrientation.getOrientation(cameraPos, height, height + CLOUD_HEIGHT); + } else { + // When fast clouds are used, there is no orientation of faces, since culling is disabled. + // To avoid unnecessary rebuilds, simply mark a null (undefined) orientation. + orientation = null; + } + + var parameters = new CloudGeometryParameters(cellX, cellZ, renderDistance, orientation, renderMode); + CloudGeometry geometry = this.builtGeometry; + + // Re-generate the cached cloud geometry if necessary if (geometry == null || !Objects.equals(geometry.params(), parameters)) { - this.cachedGeometry = (geometry = rebuildGeometry(geometry, parameters, this.textureData)); + this.builtGeometry = (geometry = rebuildGeometry(geometry, parameters, this.textureData)); } VertexBuffer vertexBuffer = geometry.vertexBuffer(); + + // The vertex buffer can be empty when there are no clouds to render if (vertexBuffer == null) { return; } - final float translateX = (float) (cloudCenterX - (centerCellX * 12)); - final float translateZ = (float) (cloudCenterZ - (centerCellZ * 12)); + // Apply world->view transform + final float viewPosX = (float) (worldX - (cellX * CLOUD_WIDTH)); + final float viewPosY = (float) cameraPos.y() - height; + final float viewPosZ = (float) (worldZ - (cellZ * CLOUD_WIDTH)); poseStack.pushPose(); - var poseEntry = poseStack.last(); Matrix4f modelViewMatrix = poseEntry.pose(); - modelViewMatrix.translate(-translateX, cloudHeight - (float) pos.y() + 0.33F, -translateZ); + modelViewMatrix.translate(-viewPosX, -viewPosY, -viewPosZ); final var prevShaderFogShape = RenderSystem.getShaderFogShape(); final var prevShaderFogEnd = RenderSystem.getShaderFogEnd(); final var prevShaderFogStart = RenderSystem.getShaderFogStart(); - FogRenderer.setupFog(camera, FogRenderer.FogMode.FOG_TERRAIN, cloudDistance * 8, shouldUseWorldFog(level, pos), tickDelta); + FogRenderer.setupFog(camera, FogRenderer.FogMode.FOG_TERRAIN, renderDistance * 8, shouldUseWorldFog(level, cameraPos), tickDelta); boolean fastClouds = geometry.params().renderMode() == CloudStatus.FAST; boolean fabulous = Minecraft.useShaderTransparency(); @@ -160,25 +196,25 @@ public void render(Camera camera, var writer = VertexBufferWriter.of(bufferBuilder); - var originCellX = parameters.originX(); - var originCellZ = parameters.originZ(); - - var orientation = parameters.orientation(); + final var radius = parameters.radius(); + final var orientation = parameters.orientation(); + final var flat = parameters.renderMode() == CloudStatus.FAST; - var radius = parameters.radius(); - var useFastGraphics = parameters.renderMode() == CloudStatus.FAST; + final var slice = textureData.slice(parameters.originX(), parameters.originZ(), radius); - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, 0, 0, orientation, useFastGraphics); + // Iterate from the center coordinates (0, 0) outwards + // Since the geometry will be in sorted order, this avoids needing a depth pre-pass + addCellGeometryToBuffer(writer, slice, 0, 0, orientation, flat); for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } } @@ -187,38 +223,43 @@ public void render(Camera camera, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } for (int z = l; z <= radius; z++) { int x = z - layer; - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } for (int z = radius; z >= l; z--) { int x = layer - z; - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - addCellGeometryToBuffer(writer, textureData, originCellX, originCellZ, x, z, orientation, useFastGraphics); + addCellGeometryToBuffer(writer, slice, x, z, orientation, flat); } } - MeshData builtBuffer = bufferBuilder.build(); + @Nullable MeshData meshData = bufferBuilder.build(); + @Nullable VertexBuffer vertexBuffer = null; - VertexBuffer vertexBuffer = null; + if (existingGeometry != null) { + vertexBuffer = existingGeometry.vertexBuffer(); + } - if (builtBuffer != null) { - if (existingGeometry != null) { - vertexBuffer = existingGeometry.vertexBuffer(); - } + if (meshData != null) { if (vertexBuffer == null) { vertexBuffer = new VertexBuffer(VertexBuffer.Usage.DYNAMIC); } - uploadToVertexBuffer(vertexBuffer, builtBuffer); + uploadToVertexBuffer(vertexBuffer, meshData); + } else { + if (vertexBuffer != null) { + vertexBuffer.close(); + vertexBuffer = null; + } } Tesselator.getInstance().clear(); @@ -227,172 +268,235 @@ public void render(Camera camera, } private static void addCellGeometryToBuffer(VertexBufferWriter writer, - CloudTextureData textureData, - int originX, - int originZ, - int offsetX, - int offsetZ, - int orientation, - boolean useFastGraphics) { - int cellX = originX + offsetX; - int cellZ = originZ + offsetZ; - - int cellIndex = textureData.getCellIndexWrapping(cellX, cellZ); - int cellFaces = textureData.getCellFaces(cellIndex) & getVisibleFaces(offsetX, offsetZ, orientation); - - if (cellFaces == 0) { + CloudTextureData.Slice textureData, + int x, + int z, + @Nullable CloudRenderer.ViewOrientation orientation, + boolean flat) + { + int index = textureData.getCellIndex(x, z); + int faces = textureData.getCellFaces(index) & getVisibleFaces(x, z, orientation); + + if (faces == 0) { return; } - int cellColor = textureData.getCellColor(cellIndex); + int color = textureData.getCellColor(index); - if (ColorABGR.unpackAlpha(cellColor) == 0) { + if (isTransparent(color)) { return; } - float x = offsetX * 12; - float z = offsetZ * 12; - - if (useFastGraphics) { - emitCellGeometry2D(writer, cellFaces, cellColor, x, z); + if (flat) { + emitCellGeometryFlat(writer, color, x, z); } else { - emitCellGeometry3D(writer, cellFaces, cellColor, x, z, false); - - int distance = Math.abs(offsetX) + Math.abs(offsetZ); + emitCellGeometryExterior(writer, faces, color, x, z); - if (distance <= 1) { - emitCellGeometry3D(writer, CloudFaceSet.all(), cellColor, x, z, true); + if (taxicabDistance(x, z) <= 1) { + emitCellGeometryInterior(writer, color, x, z); } } } - private static int getVisibleFaces(int x, int z, int orientation) { - int faces = CloudFaceSet.all(); + private static int getVisibleFaces(int x, int z, ViewOrientation orientation) { + int faces = 0; - if (x > 0) { - faces = CloudFaceSet.remove(faces, CloudFace.POS_X); + if (x <= 0) { + faces |= FACE_MASK_POS_X; } - if (z > 0) { - faces = CloudFaceSet.remove(faces, CloudFace.POS_Z); + if (z <= 0) { + faces |= FACE_MASK_POS_Z; } - if (x < 0) { - faces = CloudFaceSet.remove(faces, CloudFace.NEG_X); + if (x >= 0) { + faces |= FACE_MASK_NEG_X; } - if (z < 0) { - faces = CloudFaceSet.remove(faces, CloudFace.NEG_Z); + if (z >= 0) { + faces |= FACE_MASK_NEG_Z; } - if (orientation < 0) { - faces = CloudFaceSet.remove(faces, CloudFace.POS_Y); + if (orientation != ViewOrientation.BELOW_CLOUDS) { + faces |= FACE_MASK_POS_Y; } - if (orientation > 0) { - faces = CloudFaceSet.remove(faces, CloudFace.NEG_Y); + if (orientation != ViewOrientation.ABOVE_CLOUDS) { + faces |= FACE_MASK_NEG_Y; } return faces; } - private static final Vector3f[][] VERTICES = new Vector3f[CloudFace.COUNT][]; - - static { - VERTICES[CloudFace.NEG_Y.ordinal()] = new Vector3f[] { - new Vector3f(12.0f, 0.0f, 12.0f), - new Vector3f( 0.0f, 0.0f, 12.0f), - new Vector3f( 0.0f, 0.0f, 0.0f), - new Vector3f(12.0f, 0.0f, 0.0f) - }; - - VERTICES[CloudFace.POS_Y.ordinal()] = new Vector3f[] { - new Vector3f( 0.0f, 4.0f, 12.0f), - new Vector3f(12.0f, 4.0f, 12.0f), - new Vector3f(12.0f, 4.0f, 0.0f), - new Vector3f( 0.0f, 4.0f, 0.0f) - }; - - VERTICES[CloudFace.NEG_X.ordinal()] = new Vector3f[] { - new Vector3f( 0.0f, 0.0f, 12.0f), - new Vector3f( 0.0f, 4.0f, 12.0f), - new Vector3f( 0.0f, 4.0f, 0.0f), - new Vector3f( 0.0f, 0.0f, 0.0f) - }; - - VERTICES[CloudFace.POS_X.ordinal()] = new Vector3f[] { - new Vector3f(12.0f, 4.0f, 12.0f), - new Vector3f(12.0f, 0.0f, 12.0f), - new Vector3f(12.0f, 0.0f, 0.0f), - new Vector3f(12.0f, 4.0f, 0.0f) - }; - - VERTICES[CloudFace.NEG_Z.ordinal()] = new Vector3f[] { - new Vector3f(12.0f, 4.0f, 0.0f), - new Vector3f(12.0f, 0.0f, 0.0f), - new Vector3f( 0.0f, 0.0f, 0.0f), - new Vector3f( 0.0f, 4.0f, 0.0f) - }; - - VERTICES[CloudFace.POS_Z.ordinal()] = new Vector3f[] { - new Vector3f(12.0f, 0.0f, 12.0f), - new Vector3f(12.0f, 4.0f, 12.0f), - new Vector3f( 0.0f, 4.0f, 12.0f), - new Vector3f( 0.0f, 0.0f, 12.0f) - }; - } - - private static void emitCellGeometry2D(VertexBufferWriter writer, int faces, int color, float x, float z) { + private static void emitCellGeometryFlat(VertexBufferWriter writer, int texel, int x, int z) { try (MemoryStack stack = MemoryStack.stackPush()) { - final long buffer = stack.nmalloc(4 * ColorVertex.STRIDE); - - long ptr = buffer; - int count = 0; - - int mixedColor = ColorMixer.mul(color, CloudFace.POS_Y.getColor()); + final long vertexBuffer = stack.nmalloc(4 * ColorVertex.STRIDE); + long ptr = vertexBuffer; - ptr = writeVertex(ptr, x + 12.0f, 0.0f, z + 12.0f, mixedColor); - ptr = writeVertex(ptr, x + 0.0f, 0.0f, z + 12.0f, mixedColor); - ptr = writeVertex(ptr, x + 0.0f, 0.0f, z + 0.0f, mixedColor); - ptr = writeVertex(ptr, x + 12.0f, 0.0f, z + 0.0f, mixedColor); + final float x0 = (x * CLOUD_WIDTH); + final float x1 = x0 + CLOUD_WIDTH; + final float z0 = (z * CLOUD_WIDTH); + final float z1 = z0 + CLOUD_WIDTH; - count += 4; + { + final int color = ColorABGR.mulRGB(texel, BRIGHTNESS_POS_Y); + ptr = writeVertex(ptr, x1, 0.0f, z1, color); + ptr = writeVertex(ptr, x0, 0.0f, z1, color); + ptr = writeVertex(ptr, x0, 0.0f, z0, color); + ptr = writeVertex(ptr, x1, 0.0f, z0, color); + } - writer.push(stack, buffer, count, ColorVertex.FORMAT); + writer.push(stack, vertexBuffer, 4, ColorVertex.FORMAT); } } - private static void emitCellGeometry3D(VertexBufferWriter writer, int visibleFaces, int baseColor, float posX, float posZ, boolean interior) { + private static void emitCellGeometryExterior(VertexBufferWriter writer, int cellFaces, int cellColor, int cellX, int cellZ) { try (MemoryStack stack = MemoryStack.stackPush()) { - final long buffer = stack.nmalloc(6 * 4 * ColorVertex.STRIDE); + final long vertexBuffer = stack.nmalloc(6 * 4 * ColorVertex.STRIDE); + int vertexCount = 0; + + long ptr = vertexBuffer; + + final float x0 = cellX * CLOUD_WIDTH; + final float y0 = 0.0f; + final float z0 = cellZ * CLOUD_WIDTH; + + final float x1 = x0 + CLOUD_WIDTH; + final float y1 = y0 + CLOUD_HEIGHT; + final float z1 = z0 + CLOUD_WIDTH; + + // -Y + if ((cellFaces & FACE_MASK_NEG_Y) != 0) { + final int vertexColor = ColorABGR.mulRGB(cellColor, BRIGHTNESS_NEG_Y); + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + vertexCount += 4; + } - long ptr = buffer; - int count = 0; + // +Y + if ((cellFaces & FACE_MASK_POS_Y) != 0) { + final int vertexColor = ColorABGR.mulRGB(cellColor, BRIGHTNESS_POS_Y); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + vertexCount += 4; + } + + if ((cellFaces & (FACE_MASK_NEG_X | FACE_MASK_POS_X)) != 0) { + final int vertexColor = ColorABGR.mulRGB(cellColor, BRIGHTNESS_X_AXIS); - for (var face : CloudFace.VALUES) { - if (!CloudFaceSet.contains(visibleFaces, face)) { - continue; + // -X + if ((cellFaces & FACE_MASK_NEG_X) != 0) { + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + vertexCount += 4; } - final var vertices = VERTICES[face.ordinal()]; - final int color = ColorMixer.mul(baseColor, face.getColor()); + // +X + if ((cellFaces & FACE_MASK_POS_X) != 0) { + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + vertexCount += 4; + } + } - for (int vertexIndex = 0; vertexIndex < 4; vertexIndex++) { - Vector3f vertex = vertices[interior ? 3 - vertexIndex : vertexIndex]; + if ((cellFaces & (FACE_MASK_NEG_Z | FACE_MASK_POS_Z)) != 0) { + final int vertexColor = ColorABGR.mulRGB(cellColor, BRIGHTNESS_Z_AXIS); - final float x = vertex.x + posX; - final float y = vertex.y; - final float z = vertex.z + posZ; + // -Z + if ((cellFaces & FACE_MASK_NEG_Z) != 0) { + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + vertexCount += 4; + } - ptr = writeVertex(ptr, x, y, z, color); + // +Z + if ((cellFaces & FACE_MASK_POS_Z) != 0) { + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + vertexCount += 4; } + } + + writer.push(stack, vertexBuffer, vertexCount, ColorVertex.FORMAT); + } + } + + private static void emitCellGeometryInterior(VertexBufferWriter writer, int baseColor, int cellX, int cellZ) { + try (MemoryStack stack = MemoryStack.stackPush()) { + final long vertexBuffer = stack.nmalloc(6 * 4 * ColorVertex.STRIDE); + long ptr = vertexBuffer; + + final float x0 = cellX * CLOUD_WIDTH; + final float y0 = 0.0f; + final float z0 = cellZ * CLOUD_WIDTH; + + final float x1 = x0 + CLOUD_WIDTH; + final float y1 = y0 + CLOUD_HEIGHT; + final float z1 = z0 + CLOUD_WIDTH; + + { + // -Y + final int vertexColor = ColorABGR.mulRGB(baseColor, BRIGHTNESS_NEG_Y); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); + } - count += 4; + { + // +Y + final int vertexColor = ColorABGR.mulRGB(baseColor, BRIGHTNESS_POS_Y); + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); } - if (count > 0) { - writer.push(stack, buffer, count, ColorVertex.FORMAT); + { + final int vertexColor = ColorABGR.mulRGB(baseColor, BRIGHTNESS_X_AXIS); + + // -X + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + + // +X + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + } + + { + final int vertexColor = ColorABGR.mulRGB(baseColor, BRIGHTNESS_Z_AXIS); + + // -Z + ptr = writeVertex(ptr, x0, y1, z0, vertexColor); + ptr = writeVertex(ptr, x0, y0, z0, vertexColor); + ptr = writeVertex(ptr, x1, y0, z0, vertexColor); + ptr = writeVertex(ptr, x1, y1, z0, vertexColor); + + // +Z + ptr = writeVertex(ptr, x0, y0, z1, vertexColor); + ptr = writeVertex(ptr, x0, y1, z1, vertexColor); + ptr = writeVertex(ptr, x1, y1, z1, vertexColor); + ptr = writeVertex(ptr, x1, y0, z1, vertexColor); } + + writer.push(stack, vertexBuffer, 6 * 4, ColorVertex.FORMAT); } } @@ -408,16 +512,10 @@ private static void uploadToVertexBuffer(VertexBuffer vertexBuffer, MeshData bui VertexBuffer.unbind(); } - public void reloadTextures(ResourceProvider resourceProvider) { + public void reload(ResourceProvider resourceProvider) { this.destroy(); - - this.textureData = loadTextureData(); - - try { - this.shaderProgram = new ShaderInstance(resourceProvider, "clouds", DefaultVertexFormat.POSITION_COLOR); - } catch (IOException e) { - throw new RuntimeException(e); - } + this.textureData = loadTextureData(resourceProvider); + this.shaderProgram = loadShaderProgram(resourceProvider); } public void destroy() { @@ -426,25 +524,41 @@ public void destroy() { this.shaderProgram = null; } - if (this.cachedGeometry != null) { - var vertexBuffer = this.cachedGeometry.vertexBuffer(); - vertexBuffer.close(); + if (this.builtGeometry != null) { + var vertexBuffer = this.builtGeometry.vertexBuffer(); + + if (vertexBuffer != null) { + vertexBuffer.close(); + } + + this.builtGeometry = null; + } + } - this.cachedGeometry = null; + private static @Nullable ShaderInstance loadShaderProgram(ResourceProvider resourceProvider) { + try { + return new ShaderInstance(resourceProvider, "clouds", DefaultVertexFormat.POSITION_COLOR); + } catch (Throwable t) { + LOGGER.error("Failed to compile shader program for cloud rendering. The rendering of clouds in the skybox will be disabled. " + + "This may be caused by an incompatible resource pack.", t); } + + return null; } - private static CloudTextureData loadTextureData() { - ResourceManager resourceManager = Minecraft.getInstance().getResourceManager(); - Resource resource = resourceManager.getResource(CLOUDS_TEXTURE_ID) - .orElseThrow(); + private static @Nullable CloudTextureData loadTextureData(ResourceProvider resourceProvider) { + var resource = resourceProvider.getResource(CloudRenderer.CLOUDS_TEXTURE_ID) + .orElseThrow(); // always provided by default resource pack - try (InputStream inputStream = resource.open()){ - try (NativeImage nativeImage = NativeImage.read(inputStream)) { - return new CloudTextureData(nativeImage); - } - } catch (IOException ex) { - throw new RuntimeException("Failed to load texture data", ex); + try (var inputStream = resource.open(); + var nativeImage = NativeImage.read(inputStream)) + { + return CloudTextureData.load(nativeImage); + } + catch (Throwable t) { + LOGGER.error("Failed to load texture '{}'. The rendering of clouds in the skybox will be disabled. " + + "This may be caused by an incompatible resource pack.", CloudRenderer.CLOUDS_TEXTURE_ID, t); + return null; } } @@ -457,96 +571,96 @@ private static int getCloudRenderDistance() { return Math.max(32, (Minecraft.getInstance().options.getEffectiveRenderDistance() * 2) + 9); } - private enum CloudFace { - NEG_Y(ColorABGR.pack(0.7F, 0.7F, 0.7F, 1.0f)), - POS_Y(ColorABGR.pack(1.0f, 1.0f, 1.0f, 1.0f)), - NEG_X(ColorABGR.pack(0.9F, 0.9F, 0.9F, 1.0f)), - POS_X(ColorABGR.pack(0.9F, 0.9F, 0.9F, 1.0f)), - NEG_Z(ColorABGR.pack(0.8F, 0.8F, 0.8F, 1.0f)), - POS_Z(ColorABGR.pack(0.8F, 0.8F, 0.8F, 1.0f)); + private static boolean isTransparent(int argb) { + return ColorARGB.unpackAlpha(argb) < 10; + } - public static final CloudFace[] VALUES = CloudFace.values(); - public static final int COUNT = VALUES.length; + private static int taxicabDistance(int x, int z) { + return Math.abs(x) + Math.abs(z); + } - private final int color; + private static class CloudTextureData { + private final byte[] faces; + private final int[] colors; - CloudFace(int color) { - this.color = color; - } + private final int width, height; - public int getColor() { - return this.color; - } - } + private CloudTextureData(int width, int height) { + this.faces = new byte[width * height]; + this.colors = new int[width * height]; - private static class CloudFaceSet { - public static int empty() { - return 0; + this.width = width; + this.height = height; } - public static boolean contains(int set, CloudFace face) { - return (set & (1 << face.ordinal())) != 0; - } + public Slice slice(int originX, int originY, int radius) { + final var src = this; + final var dst = new CloudTextureData.Slice(radius); - public static int add(int set, CloudFace face) { - return set | (1 << face.ordinal()); - } + for (int dstY = 0; dstY < dst.height; dstY++) { + int srcX = Math.floorMod(originX - radius, this.width); + int srcY = Math.floorMod(originY - radius + dstY, this.height); - public static int remove(int set, CloudFace face) { - return set & ~(1 << face.ordinal()); - } + int dstX = 0; - public static int all() { - return (1 << CloudFace.COUNT) - 1; - } - } + while (dstX < dst.width) { + final int length = Math.min(src.width - srcX, dst.width - dstX); - private static boolean isTransparentCell(int color) { - return ColorARGB.unpackAlpha(color) <= 1; - } + final int srcPos = getCellIndex(srcX, srcY, src.width); + final int dstPos = getCellIndex(dstX, dstY, dst.width); - private static class CloudTextureData { - private final byte[] faces; - private final int[] colors; - private boolean isBlank; + System.arraycopy(this.faces, srcPos, dst.faces, dstPos, length); + System.arraycopy(this.colors, srcPos, dst.colors, dstPos, length); - private final int width, height; + srcX = 0; + dstX += length; + } + } - public CloudTextureData(NativeImage texture) { - int width = texture.getWidth(); - int height = texture.getHeight(); + return dst; + } - this.faces = new byte[width * height]; - this.colors = new int[width * height]; - this.isBlank = true; + public static @Nullable CloudTextureData load(NativeImage image) { + final int width = image.getWidth(); + final int height = image.getHeight(); - this.width = width; - this.height = height; + var data = new CloudTextureData(width, height); + + if (!data.loadTextureData(image, width, height)) { + return null; // The texture is empty, so it isn't necessary to render it + } - this.loadTextureData(texture, width, height); + return data; } - private void loadTextureData(NativeImage texture, int width, int height) { + private boolean loadTextureData(NativeImage texture, int width, int height) { + Validate.isTrue(this.width == width); + Validate.isTrue(this.height == height); + + boolean containsData = false; + for (int x = 0; x < width; x++) { for (int z = 0; z < height; z++) { - int index = this.getCellIndex(x, z); int color = texture.getPixelRGBA(x, z); + if (isTransparent(color)) { + continue; + } + + int index = getCellIndex(x, z, width); this.colors[index] = color; + this.faces[index] = (byte) getOpenFaces(texture, color, x, z); - if (!isTransparentCell(color)) { - this.faces[index] = (byte) getOpenFaces(texture, color, x, z); - this.isBlank = false; - } + containsData = true; } } + + return containsData; } private static int getOpenFaces(NativeImage image, int color, int x, int z) { // Since the cloud texture is only 2D, nothing can hide the top or bottom faces - int faces = CloudFaceSet.empty(); - faces = CloudFaceSet.add(faces, CloudFace.NEG_Y); - faces = CloudFaceSet.add(faces, CloudFace.POS_Y); + int faces = FACE_MASK_NEG_Y | FACE_MASK_POS_Y; // Generate faces where the neighbor cell is a different color // Do not generate duplicate faces between two cells @@ -555,7 +669,7 @@ private static int getOpenFaces(NativeImage image, int color, int x, int z) { int neighbor = getNeighborTexel(image, x - 1, z); if (color != neighbor) { - faces = CloudFaceSet.add(faces, CloudFace.NEG_X); + faces |= FACE_MASK_NEG_X; } } @@ -564,7 +678,7 @@ private static int getOpenFaces(NativeImage image, int color, int x, int z) { int neighbor = getNeighborTexel(image, x + 1, z); if (color != neighbor) { - faces = CloudFaceSet.add(faces, CloudFace.POS_X); + faces |= FACE_MASK_POS_X; } } @@ -573,7 +687,7 @@ private static int getOpenFaces(NativeImage image, int color, int x, int z) { int neighbor = getNeighborTexel(image, x, z - 1); if (color != neighbor) { - faces = CloudFaceSet.add(faces, CloudFace.NEG_Z); + faces |= FACE_MASK_NEG_Z; } } @@ -582,7 +696,7 @@ private static int getOpenFaces(NativeImage image, int color, int x, int z) { int neighbor = getNeighborTexel(image, x, z + 1); if (color != neighbor) { - faces = CloudFaceSet.add(faces, CloudFace.POS_Z); + faces |= FACE_MASK_POS_Z; } } @@ -608,31 +722,59 @@ private static int wrapTexelCoord(int coord, int min, int max) { return coord; } - public int getCellFaces(int index) { - return this.faces[index]; + private static int getCellIndex(int x, int z, int pitch) { + return (z * pitch) + x; } - public int getCellColor(int index) { - return this.colors[index]; - } + public static class Slice { + private final int width, height; + private final int radius; + private final byte[] faces; + private final int[] colors; - private int getCellIndexWrapping(int x, int z) { - return this.getCellIndex( - Math.floorMod(x, this.width), - Math.floorMod(z, this.height) - ); - } + public Slice(int radius) { + this.width = 1 + (radius * 2); + this.height = 1 + (radius * 2); + this.radius = radius; + this.faces = new byte[this.width * this.height]; + this.colors = new int[this.width * this.height]; + } + + public int getCellIndex(int x, int z) { + return CloudTextureData.getCellIndex(x + this.radius, z + this.radius, this.width); + } - private int getCellIndex(int x, int z) { - return (x * this.width) + z; + public int getCellFaces(int index) { + return Byte.toUnsignedInt(this.faces[index]); + } + + public int getCellColor(int index) { + return this.colors[index]; + } } } - public record CloudGeometry(VertexBuffer vertexBuffer, CloudGeometryParameters params) { + public record CloudGeometry(@Nullable VertexBuffer vertexBuffer, CloudGeometryParameters params) { } - public record CloudGeometryParameters(int originX, int originZ, int radius, int orientation, CloudStatus renderMode) { + public record CloudGeometryParameters(int originX, int originZ, int radius, @Nullable ViewOrientation orientation, CloudStatus renderMode) { } + + private enum ViewOrientation { + BELOW_CLOUDS, // Top faces should *not* be rendered + INSIDE_CLOUDS, // All faces *must* be rendered + ABOVE_CLOUDS; // Bottom faces should *not* be rendered + + public static @NotNull ViewOrientation getOrientation(Vec3 camera, float minY, float maxY) { + if (camera.y() <= minY + 0.125f /* epsilon */) { + return ViewOrientation.BELOW_CLOUDS; + } else if (camera.y() >= maxY - 0.125f /* epsilon */) { + return ViewOrientation.ABOVE_CLOUDS; + } else { + return ViewOrientation.INSIDE_CLOUDS; + } + } + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/BakedModelEncoder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/BakedModelEncoder.java index 3aadf5ca67..145b71d1e3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/BakedModelEncoder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/BakedModelEncoder.java @@ -1,13 +1,13 @@ package net.caffeinemc.mods.sodium.client.render.immediate.model; import com.mojang.blaze3d.vertex.PoseStack; +import net.caffeinemc.mods.sodium.api.util.ColorMixer; import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; import net.caffeinemc.mods.sodium.api.math.MatrixHelper; import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.util.ColorU8; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; -import net.caffeinemc.mods.sodium.client.render.frapi.helper.ColorHelper; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import org.joml.Matrix3f; import org.joml.Matrix4f; @@ -43,7 +43,7 @@ public static void writeQuadVertices(VertexBufferWriter writer, PoseStack.Pose m int newColor = color; if (colorize) { - newColor = ColorHelper.multiplyColor(newColor, quad.getColor(i)); + newColor = ColorMixer.mulComponentWise(newColor, quad.getColor(i)); } // The packed transformed normal vector diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/EntityRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/EntityRenderer.java index 0c618b8204..2e7ed9076e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/EntityRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/EntityRenderer.java @@ -4,111 +4,62 @@ import net.caffeinemc.mods.sodium.api.math.MatrixHelper; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; +import net.caffeinemc.mods.sodium.client.util.Int2; import net.minecraft.core.Direction; import org.joml.Matrix3f; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector3f; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; import static net.caffeinemc.mods.sodium.client.render.immediate.model.ModelCuboid.*; public class EntityRenderer { - private static final int NUM_CUBE_VERTICES = 8; - private static final int NUM_CUBE_FACES = 6; - private static final int NUM_FACE_VERTICES = 4; - - private static final int - VERTEX_X1_Y1_Z1 = 0, - VERTEX_X2_Y1_Z1 = 1, - VERTEX_X2_Y2_Z1 = 2, - VERTEX_X1_Y2_Z1 = 3, - VERTEX_X1_Y1_Z2 = 4, - VERTEX_X2_Y1_Z2 = 5, - VERTEX_X2_Y2_Z2 = 6, - VERTEX_X1_Y2_Z2 = 7; - - private static final Matrix3f lastMatrix = new Matrix3f(); - - private static final long SCRATCH_BUFFER = MemoryUtil.nmemAlignedAlloc(64, NUM_CUBE_FACES * NUM_FACE_VERTICES * EntityVertex.STRIDE); - - private static final Vector3f[] CUBE_CORNERS = new Vector3f[NUM_CUBE_VERTICES]; - private static final int[][] CUBE_VERTICES = new int[NUM_CUBE_FACES][]; - - private static final Vector3f[][] VERTEX_POSITIONS = new Vector3f[NUM_CUBE_FACES][NUM_FACE_VERTICES]; - private static final Vector3f[][] VERTEX_POSITIONS_MIRRORED = new Vector3f[NUM_CUBE_FACES][NUM_FACE_VERTICES]; - - private static final Vector2f[][] VERTEX_TEXTURES = new Vector2f[NUM_CUBE_FACES][NUM_FACE_VERTICES]; - private static final Vector2f[][] VERTEX_TEXTURES_MIRRORED = new Vector2f[NUM_CUBE_FACES][NUM_FACE_VERTICES]; - - private static final int[] CUBE_NORMALS = new int[NUM_CUBE_FACES]; - private static final int[] CUBE_NORMALS_MIRRORED = new int[NUM_CUBE_FACES]; - - static { - CUBE_VERTICES[FACE_NEG_Y] = new int[] { VERTEX_X2_Y1_Z2, VERTEX_X1_Y1_Z2, VERTEX_X1_Y1_Z1, VERTEX_X2_Y1_Z1 }; - CUBE_VERTICES[FACE_POS_Y] = new int[] { VERTEX_X2_Y2_Z1, VERTEX_X1_Y2_Z1, VERTEX_X1_Y2_Z2, VERTEX_X2_Y2_Z2 }; - CUBE_VERTICES[FACE_NEG_Z] = new int[] { VERTEX_X2_Y1_Z1, VERTEX_X1_Y1_Z1, VERTEX_X1_Y2_Z1, VERTEX_X2_Y2_Z1 }; - CUBE_VERTICES[FACE_POS_Z] = new int[] { VERTEX_X1_Y1_Z2, VERTEX_X2_Y1_Z2, VERTEX_X2_Y2_Z2, VERTEX_X1_Y2_Z2 }; - CUBE_VERTICES[FACE_NEG_X] = new int[] { VERTEX_X2_Y1_Z2, VERTEX_X2_Y1_Z1, VERTEX_X2_Y2_Z1, VERTEX_X2_Y2_Z2 }; - CUBE_VERTICES[FACE_POS_X] = new int[] { VERTEX_X1_Y1_Z1, VERTEX_X1_Y1_Z2, VERTEX_X1_Y2_Z2, VERTEX_X1_Y2_Z1 }; - - for (int cornerIndex = 0; cornerIndex < NUM_CUBE_VERTICES; cornerIndex++) { - CUBE_CORNERS[cornerIndex] = new Vector3f(); - } + private static final Matrix3f prevNormalMatrix = new Matrix3f(); - for (int quadIndex = 0; quadIndex < NUM_CUBE_FACES; quadIndex++) { - for (int vertexIndex = 0; vertexIndex < NUM_FACE_VERTICES; vertexIndex++) { - VERTEX_TEXTURES[quadIndex][vertexIndex] = new Vector2f(); - VERTEX_POSITIONS[quadIndex][vertexIndex] = CUBE_CORNERS[CUBE_VERTICES[quadIndex][vertexIndex]]; - } - } + private static final int VERTEX_BUFFER_BYTES = NUM_CUBE_FACES * NUM_FACE_VERTICES * EntityVertex.STRIDE; - for (int quadIndex = 0; quadIndex < NUM_CUBE_FACES; quadIndex++) { - for (int vertexIndex = 0; vertexIndex < NUM_FACE_VERTICES; vertexIndex++) { - VERTEX_TEXTURES_MIRRORED[quadIndex][vertexIndex] = VERTEX_TEXTURES[quadIndex][3 - vertexIndex]; - VERTEX_POSITIONS_MIRRORED[quadIndex][vertexIndex] = VERTEX_POSITIONS[quadIndex][3 - vertexIndex]; - } - } - } + private static final long[] CUBE_VERTEX_XY = new long[NUM_CUBE_VERTICES]; // (pos.x, pos.y) + private static final long[] CUBE_VERTEX_ZW = new long[NUM_CUBE_VERTICES]; // (pos.z, color) + + private static final int[] CUBE_FACE_NORMAL = new int[NUM_CUBE_FACES]; public static void renderCuboid(PoseStack.Pose matrices, VertexBufferWriter writer, ModelCuboid cuboid, int light, int overlay, int color) { + prepareVertices(matrices, cuboid, color); prepareNormalsIfChanged(matrices); - prepareVertices(matrices, cuboid); - - var vertexCount = emitQuads(cuboid, color, overlay, light); - try (MemoryStack stack = MemoryStack.stackPush()) { - writer.push(stack, SCRATCH_BUFFER, vertexCount, EntityVertex.FORMAT); + final var vertexBuffer = stack.nmalloc(64, VERTEX_BUFFER_BYTES); + final var vertexCount = emitQuads(vertexBuffer, cuboid, overlay, light); + + if (vertexCount > 0) { + writer.push(stack, vertexBuffer, vertexCount, EntityVertex.FORMAT); + } } } - private static int emitQuads(ModelCuboid cuboid, int color, int overlay, int light) { - final var positions = cuboid.mirror ? VERTEX_POSITIONS_MIRRORED : VERTEX_POSITIONS; - final var textures = cuboid.mirror ? VERTEX_TEXTURES_MIRRORED : VERTEX_TEXTURES; - final var normals = cuboid.mirror ? CUBE_NORMALS_MIRRORED : CUBE_NORMALS; + private static int emitQuads(final long buffer, ModelCuboid cuboid, int overlay, int light) { + // Pack the Overlay and Light coordinates into a 64-bit integer as they are next to each other + // in the vertex format. This eliminates another 32-bit memory write in the hot path. + final long packedOverlayLight = Int2.pack(overlay, light); - var vertexCount = 0; + long ptr = buffer; - long ptr = SCRATCH_BUFFER; + final int[] normals = cuboid.normals; + final int[] positions = cuboid.positions; + final long[] textures = cuboid.textures; - for (int quadIndex = 0; quadIndex < NUM_CUBE_FACES; quadIndex++) { - if (!cuboid.shouldDrawFace(quadIndex)) { + int vertexCount = 0; + + for (int faceIndex = 0; faceIndex < NUM_CUBE_FACES; faceIndex++) { + if (!cuboid.shouldDrawFace(faceIndex)) { continue; } - - emitVertex(ptr, positions[quadIndex][0], color, textures[quadIndex][0], overlay, light, normals[quadIndex]); - ptr += EntityVertex.STRIDE; - - emitVertex(ptr, positions[quadIndex][1], color, textures[quadIndex][1], overlay, light, normals[quadIndex]); - ptr += EntityVertex.STRIDE; - - emitVertex(ptr, positions[quadIndex][2], color, textures[quadIndex][2], overlay, light, normals[quadIndex]); - ptr += EntityVertex.STRIDE; - - emitVertex(ptr, positions[quadIndex][3], color, textures[quadIndex][3], overlay, light, normals[quadIndex]); - ptr += EntityVertex.STRIDE; + + final int elementOffset = faceIndex * NUM_FACE_VERTICES; + final int packedNormal = CUBE_FACE_NORMAL[normals[faceIndex]]; + ptr = writeVertex(ptr, positions[elementOffset + 0], textures[elementOffset + 0], packedOverlayLight, packedNormal); + ptr = writeVertex(ptr, positions[elementOffset + 1], textures[elementOffset + 1], packedOverlayLight, packedNormal); + ptr = writeVertex(ptr, positions[elementOffset + 2], textures[elementOffset + 2], packedOverlayLight, packedNormal); + ptr = writeVertex(ptr, positions[elementOffset + 3], textures[elementOffset + 3], packedOverlayLight, packedNormal); vertexCount += 4; } @@ -116,59 +67,89 @@ private static int emitQuads(ModelCuboid cuboid, int color, int overlay, int lig return vertexCount; } - private static void emitVertex(long ptr, Vector3f pos, int color, Vector2f tex, int overlay, int light, int normal) { - EntityVertex.write(ptr, pos.x, pos.y, pos.z, color, tex.x, tex.y, overlay, light, normal); - } + private static long writeVertex(long ptr, int vertexIndex, long packedUv, long packedOverlayLight, int packedNormal) { + MemoryUtil.memPutLong(ptr + 0L, CUBE_VERTEX_XY[vertexIndex]); + MemoryUtil.memPutLong(ptr + 8L, CUBE_VERTEX_ZW[vertexIndex]); // overlaps with color attribute + MemoryUtil.memPutLong(ptr + 16L, packedUv); + MemoryUtil.memPutLong(ptr + 24L, packedOverlayLight); + MemoryUtil.memPutInt(ptr + 32L, packedNormal); - private static void prepareVertices(PoseStack.Pose matrices, ModelCuboid cuboid) { - buildVertexPosition(CUBE_CORNERS[VERTEX_X1_Y1_Z1], cuboid.x1, cuboid.y1, cuboid.z1, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X2_Y1_Z1], cuboid.x2, cuboid.y1, cuboid.z1, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X2_Y2_Z1], cuboid.x2, cuboid.y2, cuboid.z1, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X1_Y2_Z1], cuboid.x1, cuboid.y2, cuboid.z1, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X1_Y1_Z2], cuboid.x1, cuboid.y1, cuboid.z2, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X2_Y1_Z2], cuboid.x2, cuboid.y1, cuboid.z2, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X2_Y2_Z2], cuboid.x2, cuboid.y2, cuboid.z2, matrices.pose()); - buildVertexPosition(CUBE_CORNERS[VERTEX_X1_Y2_Z2], cuboid.x1, cuboid.y2, cuboid.z2, matrices.pose()); - - buildVertexTexCoord(VERTEX_TEXTURES[FACE_NEG_Y], cuboid.u1, cuboid.v0, cuboid.u2, cuboid.v1); - buildVertexTexCoord(VERTEX_TEXTURES[FACE_POS_Y], cuboid.u2, cuboid.v1, cuboid.u3, cuboid.v0); - buildVertexTexCoord(VERTEX_TEXTURES[FACE_NEG_Z], cuboid.u1, cuboid.v1, cuboid.u2, cuboid.v2); - buildVertexTexCoord(VERTEX_TEXTURES[FACE_POS_Z], cuboid.u4, cuboid.v1, cuboid.u5, cuboid.v2); - buildVertexTexCoord(VERTEX_TEXTURES[FACE_NEG_X], cuboid.u2, cuboid.v1, cuboid.u4, cuboid.v2); - buildVertexTexCoord(VERTEX_TEXTURES[FACE_POS_X], cuboid.u0, cuboid.v1, cuboid.u1, cuboid.v2); + return ptr + EntityVertex.STRIDE; } - public static void prepareNormalsIfChanged(PoseStack.Pose matrices) { - if (!matrices.normal().equals(lastMatrix)) { - lastMatrix.set(matrices.normal()); - - CUBE_NORMALS[FACE_NEG_Y] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.DOWN); - CUBE_NORMALS[FACE_POS_Y] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.UP); - CUBE_NORMALS[FACE_NEG_Z] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.NORTH); - CUBE_NORMALS[FACE_POS_Z] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.SOUTH); - CUBE_NORMALS[FACE_POS_X] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.WEST); - CUBE_NORMALS[FACE_NEG_X] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.EAST); - - // When mirroring is used, the normals for EAST and WEST are swapped. - CUBE_NORMALS_MIRRORED[FACE_NEG_Y] = CUBE_NORMALS[FACE_NEG_Y]; - CUBE_NORMALS_MIRRORED[FACE_POS_Y] = CUBE_NORMALS[FACE_POS_Y]; - CUBE_NORMALS_MIRRORED[FACE_NEG_Z] = CUBE_NORMALS[FACE_NEG_Z]; - CUBE_NORMALS_MIRRORED[FACE_POS_Z] = CUBE_NORMALS[FACE_POS_Z]; - CUBE_NORMALS_MIRRORED[FACE_POS_X] = CUBE_NORMALS[FACE_NEG_X]; // mirrored - CUBE_NORMALS_MIRRORED[FACE_NEG_X] = CUBE_NORMALS[FACE_POS_X]; // mirrored - } + private static void prepareVertices(PoseStack.Pose matrices, ModelCuboid cuboid, int color) { + var pose = matrices.pose(); + + float vxx = (pose.m00() * cuboid.sizeX), vxy = (pose.m01() * cuboid.sizeX), vxz = (pose.m02() * cuboid.sizeX); + float vyx = (pose.m10() * cuboid.sizeY), vyy = (pose.m11() * cuboid.sizeY), vyz = (pose.m12() * cuboid.sizeY); + float vzx = (pose.m20() * cuboid.sizeZ), vzy = (pose.m21() * cuboid.sizeZ), vzz = (pose.m22() * cuboid.sizeZ); + + // Compute the transformed origin point of the cuboid + float c000x = MatrixHelper.transformPositionX(pose, cuboid.originX, cuboid.originY, cuboid.originZ); + float c000y = MatrixHelper.transformPositionY(pose, cuboid.originX, cuboid.originY, cuboid.originZ); + float c000z = MatrixHelper.transformPositionZ(pose, cuboid.originX, cuboid.originY, cuboid.originZ); + setVertex(VERTEX_X0_Y0_Z0, c000x, c000y, c000z, color); + + // Add the pre-multiplied vectors to find the other 7 vertices + // This avoids needing to multiply each vertex position against the pose matrix, which eliminates many + // floating-point operations (going from 21 flops/vert to 3 flops/vert). + // Originally suggested by MoePus on GitHub in this pull request: + // https://github.com/CaffeineMC/sodium/pull/2960 + float c100x = c000x + vxx; + float c100y = c000y + vxy; + float c100z = c000z + vxz; + setVertex(VERTEX_X1_Y0_Z0, c100x, c100y, c100z, color); + + float c110x = c100x + vyx; + float c110y = c100y + vyy; + float c110z = c100z + vyz; + setVertex(VERTEX_X1_Y1_Z0, c110x, c110y, c110z, color); + + float c010x = c000x + vyx; + float c010y = c000y + vyy; + float c010z = c000z + vyz; + setVertex(VERTEX_X0_Y1_Z0, c010x, c010y, c010z, color); + + float c001x = c000x + vzx; + float c001y = c000y + vzy; + float c001z = c000z + vzz; + setVertex(VERTEX_X0_Y0_Z1, c001x, c001y, c001z, color); + + float c101x = c100x + vzx; + float c101y = c100y + vzy; + float c101z = c100z + vzz; + setVertex(VERTEX_X1_Y0_Z1, c101x, c101y, c101z, color); + + float c111x = c110x + vzx; + float c111y = c110y + vzy; + float c111z = c110z + vzz; + setVertex(VERTEX_X1_Y1_Z1, c111x, c111y, c111z, color); + + float c011x = c010x + vzx; + float c011y = c010y + vzy; + float c011z = c010z + vzz; + setVertex(VERTEX_X0_Y1_Z1, c011x, c011y, c011z, color); } - private static void buildVertexPosition(Vector3f vector, float x, float y, float z, Matrix4f matrix) { - vector.x = MatrixHelper.transformPositionX(matrix, x, y, z); - vector.y = MatrixHelper.transformPositionY(matrix, x, y, z); - vector.z = MatrixHelper.transformPositionZ(matrix, x, y, z); + private static void setVertex(int vertexIndex, float x, float y, float z, int color) { + // Since we have a spare element, pack the color into it. This makes the code a little obtuse, + // but it avoids another 32-bit memory write in the hot path, which helps a lot. + CUBE_VERTEX_XY[vertexIndex] = Int2.pack(Float.floatToRawIntBits(x), Float.floatToRawIntBits(y)); + CUBE_VERTEX_ZW[vertexIndex] = Int2.pack(Float.floatToRawIntBits(z), color); } - private static void buildVertexTexCoord(Vector2f[] uvs, float u1, float v1, float u2, float v2) { - uvs[0].set(u2, v1); - uvs[1].set(u1, v1); - uvs[2].set(u1, v2); - uvs[3].set(u2, v2); + private static void prepareNormalsIfChanged(PoseStack.Pose matrices) { + if (matrices.normal().equals(prevNormalMatrix)) { + return; + } + + CUBE_FACE_NORMAL[FACE_NEG_Y] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.DOWN); + CUBE_FACE_NORMAL[FACE_POS_Y] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.UP); + CUBE_FACE_NORMAL[FACE_NEG_Z] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.NORTH); + CUBE_FACE_NORMAL[FACE_POS_Z] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.SOUTH); + CUBE_FACE_NORMAL[FACE_POS_X] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.WEST); + CUBE_FACE_NORMAL[FACE_NEG_X] = MatrixHelper.transformNormal(matrices.normal(), matrices.trustedNormals, Direction.EAST); + + prevNormalMatrix.set(matrices.normal()); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/ModelCuboid.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/ModelCuboid.java index 4a13b857ca..bd77b30850 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/ModelCuboid.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/immediate/model/ModelCuboid.java @@ -1,29 +1,49 @@ package net.caffeinemc.mods.sodium.client.render.immediate.model; import java.util.Set; + +import net.caffeinemc.mods.sodium.client.util.Int2; import net.minecraft.core.Direction; +import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.NotNull; public class ModelCuboid { + public static final int NUM_CUBE_VERTICES = 8; + public static final int NUM_CUBE_FACES = 6; + public static final int NUM_FACE_VERTICES = 4; + + public static final int + VERTEX_X0_Y0_Z0 = 0, + VERTEX_X1_Y0_Z0 = 1, + VERTEX_X1_Y1_Z0 = 2, + VERTEX_X0_Y1_Z0 = 3, + VERTEX_X0_Y0_Z1 = 4, + VERTEX_X1_Y0_Z1 = 5, + VERTEX_X1_Y1_Z1 = 6, + VERTEX_X0_Y1_Z1 = 7; + // The ordering needs to be the same as Minecraft, otherwise some core shader replacements // will be unable to identify the facing. public static final int FACE_NEG_Y = 0, // DOWN FACE_POS_Y = 1, // UP - FACE_NEG_X = 2, // WEST + FACE_NEG_X = 2, // EAST FACE_NEG_Z = 3, // NORTH - FACE_POS_X = 4, // EAST + FACE_POS_X = 4, // WEST FACE_POS_Z = 5; // SOUTH - public final float x1, y1, z1; - public final float x2, y2, z2; + public final float originX, originY, originZ; + public final float sizeX, sizeY, sizeZ; - public final float u0, u1, u2, u3, u4, u5; - public final float v0, v1, v2; + // Bit-mask of visible faces + private final int cullMask; - private final int cullBitmask; + // Per-face attributes + public final int[] normals; - public final boolean mirror; + // Per-vertex attributes + public final int[] positions; + public final long[] textures; // UVs are packed into 64-bit integers to reduce the number of memory loads public ModelCuboid(int u, int v, float x1, float y1, float z1, @@ -50,51 +70,121 @@ public ModelCuboid(int u, int v, x1 = tmp; } - this.x1 = x1 / 16.0f; - this.y1 = y1 / 16.0f; - this.z1 = z1 / 16.0f; + x1 /= 16.0f; + y1 /= 16.0f; + z1 /= 16.0f; - this.x2 = x2 / 16.0f; - this.y2 = y2 / 16.0f; - this.z2 = z2 / 16.0f; + x2 /= 16.0f; + y2 /= 16.0f; + z2 /= 16.0f; + + this.originX = x1; + this.originY = y1; + this.originZ = z1; + + this.sizeX = x2 - x1; + this.sizeY = y2 - y1; + this.sizeZ = z2 - z1; var scaleU = 1.0f / textureWidth; var scaleV = 1.0f / textureHeight; - this.u0 = scaleU * (u); - this.u1 = scaleU * (u + sizeZ); - this.u2 = scaleU * (u + sizeZ + sizeX); - this.u3 = scaleU * (u + sizeZ + sizeX + sizeX); - this.u4 = scaleU * (u + sizeZ + sizeX + sizeZ); - this.u5 = scaleU * (u + sizeZ + sizeX + sizeZ + sizeX); + float u0 = scaleU * (u); + float u1 = scaleU * (u + sizeZ); + float u2 = scaleU * (u + sizeZ + sizeX); + float u3 = scaleU * (u + sizeZ + sizeX + sizeX); + float u4 = scaleU * (u + sizeZ + sizeX + sizeZ); + float u5 = scaleU * (u + sizeZ + sizeX + sizeZ + sizeX); + + float v0 = scaleV * (v); + float v1 = scaleV * (v + sizeZ); + float v2 = scaleV * (v + sizeZ + sizeY); + + this.cullMask = createCullMask(renderDirections); + + final int[] positions = new int[NUM_CUBE_FACES * NUM_FACE_VERTICES]; + final long[] textures = new long[NUM_CUBE_FACES * NUM_FACE_VERTICES]; + final int[] normals = new int[] { FACE_NEG_Y, FACE_POS_Y, FACE_NEG_X, FACE_NEG_Z, FACE_POS_X, FACE_POS_Z }; + + writeVertexList(positions, FACE_NEG_Y, VERTEX_X1_Y0_Z1, VERTEX_X0_Y0_Z1, VERTEX_X0_Y0_Z0, VERTEX_X1_Y0_Z0); + writeTexCoords(textures, FACE_NEG_Y, u1, v0, u2, v1); + + writeVertexList(positions, FACE_POS_Y, VERTEX_X1_Y1_Z0, VERTEX_X0_Y1_Z0, VERTEX_X0_Y1_Z1, VERTEX_X1_Y1_Z1); + writeTexCoords(textures, FACE_POS_Y, u2, v1, u3, v0); + + writeVertexList(positions, FACE_NEG_Z, VERTEX_X1_Y0_Z0, VERTEX_X0_Y0_Z0, VERTEX_X0_Y1_Z0, VERTEX_X1_Y1_Z0); + writeTexCoords(textures, FACE_NEG_Z, u1, v1, u2, v2); + + writeVertexList(positions, FACE_POS_Z, VERTEX_X0_Y0_Z1, VERTEX_X1_Y0_Z1, VERTEX_X1_Y1_Z1, VERTEX_X0_Y1_Z1); + writeTexCoords(textures, FACE_POS_Z, u4, v1, u5, v2); + + writeVertexList(positions, FACE_NEG_X, VERTEX_X1_Y0_Z1, VERTEX_X1_Y0_Z0, VERTEX_X1_Y1_Z0, VERTEX_X1_Y1_Z1); + writeTexCoords(textures, FACE_NEG_X, u2, v1, u4, v2); - this.v0 = scaleV * (v); - this.v1 = scaleV * (v + sizeZ); - this.v2 = scaleV * (v + sizeZ + sizeY); + writeVertexList(positions, FACE_POS_X, VERTEX_X0_Y0_Z0, VERTEX_X0_Y0_Z1, VERTEX_X0_Y1_Z1, VERTEX_X0_Y1_Z0); + writeTexCoords(textures, FACE_POS_X, u0, v1, u1, v2); - this.mirror = mirror; + if (mirror) { + reverseVertices(positions, textures); + + // When mirroring is used, the normals for EAST and WEST are swapped. + normals[FACE_POS_X] = FACE_NEG_X; + normals[FACE_NEG_X] = FACE_POS_X; + } + + this.normals = normals; + this.positions = positions; + this.textures = textures; + } + + private static int createCullMask(Set directions) { + int mask = 0; + + for (var direction : directions) { + mask |= 1 << getFaceIndex(direction); + } + + return mask; + } + + private static void reverseVertices(int[] vertices, long[] texCoords) { + for (int faceIndex = 0; faceIndex < NUM_CUBE_FACES; faceIndex++) { + final int vertexOffset = faceIndex * NUM_FACE_VERTICES; - int cullBitmask = 0; + ArrayUtils.swap(vertices, vertexOffset + 0, vertexOffset + 3); + ArrayUtils.swap(vertices, vertexOffset + 1, vertexOffset + 2); - for (var direction : renderDirections) { - cullBitmask |= 1 << getFaceIndex(direction); + ArrayUtils.swap(texCoords, vertexOffset + 0, vertexOffset + 3); + ArrayUtils.swap(texCoords, vertexOffset + 1, vertexOffset + 2); } + } + + private static void writeVertexList(int[] positions, int faceIndex, int i0, int i1, int i2, int i3) { + positions[(faceIndex * 4) + 0] = i0; + positions[(faceIndex * 4) + 1] = i1; + positions[(faceIndex * 4) + 2] = i2; + positions[(faceIndex * 4) + 3] = i3; + } - this.cullBitmask = cullBitmask; + private static void writeTexCoords(long[] textures, int faceIndex, float u1, float v1, float u2, float v2) { + textures[(faceIndex * 4) + 0] = Int2.pack(Float.floatToRawIntBits(u2), Float.floatToRawIntBits(v1)); + textures[(faceIndex * 4) + 1] = Int2.pack(Float.floatToRawIntBits(u1), Float.floatToRawIntBits(v1)); + textures[(faceIndex * 4) + 2] = Int2.pack(Float.floatToRawIntBits(u1), Float.floatToRawIntBits(v2)); + textures[(faceIndex * 4) + 3] = Int2.pack(Float.floatToRawIntBits(u2), Float.floatToRawIntBits(v2)); } public boolean shouldDrawFace(int faceIndex) { - return (this.cullBitmask & (1 << faceIndex)) != 0; + return (this.cullMask & (1 << faceIndex)) != 0; } - public static int getFaceIndex(@NotNull Direction dir) { + private static int getFaceIndex(@NotNull Direction dir) { return switch (dir) { - case DOWN -> FACE_NEG_Y; - case UP -> FACE_POS_Y; - case NORTH -> FACE_NEG_Z; - case SOUTH -> FACE_POS_Z; - case WEST -> FACE_NEG_X; - case EAST -> FACE_POS_X; + case DOWN -> FACE_NEG_Y; + case UP -> FACE_POS_Y; + case NORTH -> FACE_NEG_Z; + case SOUTH -> FACE_POS_Z; + case WEST -> FACE_POS_X; + case EAST -> FACE_NEG_X; }; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtil.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtil.java index 084acc18b5..09e0369b96 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtil.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtil.java @@ -3,18 +3,22 @@ import net.minecraft.client.renderer.texture.TextureAtlasSprite; import org.jetbrains.annotations.Nullable; +// Kept for mod compatibility, to be removed in next major release. +@Deprecated(forRemoval = true) public class SpriteUtil { + @Deprecated(forRemoval = true) public static void markSpriteActive(@Nullable TextureAtlasSprite sprite) { - if (sprite == null) { - // Can happen in some cases, for example if a mod passes a BakedQuad with a null sprite - // to a VertexConsumer that does not have a texture element. - return; + if (sprite != null) { + net.caffeinemc.mods.sodium.api.texture.SpriteUtil.INSTANCE.markSpriteActive(sprite); } - - ((SpriteContentsExtension) sprite.contents()).sodium$setActive(true); } - public static boolean hasAnimation(TextureAtlasSprite sprite) { - return ((SpriteContentsExtension) sprite.contents()).sodium$hasAnimation(); + @Deprecated(forRemoval = true) + public static boolean hasAnimation(@Nullable TextureAtlasSprite sprite) { + if (sprite != null) { + return net.caffeinemc.mods.sodium.api.texture.SpriteUtil.INSTANCE.hasAnimation(sprite); + } + + return false; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtilImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtilImpl.java new file mode 100644 index 0000000000..2c03d76e06 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/texture/SpriteUtilImpl.java @@ -0,0 +1,23 @@ +package net.caffeinemc.mods.sodium.client.render.texture; + +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class SpriteUtilImpl implements SpriteUtil { + @Override + public void markSpriteActive(@NotNull TextureAtlasSprite sprite) { + Objects.requireNonNull(sprite); + + ((SpriteContentsExtension) sprite.contents()).sodium$setActive(true); + } + + @Override + public boolean hasAnimation(@NotNull TextureAtlasSprite sprite) { + Objects.requireNonNull(sprite); + + return ((SpriteContentsExtension) sprite.contents()).sodium$hasAnimation(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/util/RenderAsserts.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/util/RenderAsserts.java index ea1ba9021c..a20c687c5b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/util/RenderAsserts.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/util/RenderAsserts.java @@ -12,7 +12,8 @@ public class RenderAsserts { */ public static boolean validateCurrentThread() { if (!RenderSystem.isOnRenderThread()) { - throw new IllegalStateException("Accessing OpenGL functions from outside the main render thread is not supported when using Sodium"); + throw new IllegalStateException("Tried to access render state from outside the main render thread! " + + "This was very likely caused by another misbehaving mod -- make sure to examine the stack trace below."); } return true; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/VertexConsumerUtils.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/VertexConsumerUtils.java index 6118d3df41..35411e9f64 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/VertexConsumerUtils.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/VertexConsumerUtils.java @@ -1,7 +1,9 @@ package net.caffeinemc.mods.sodium.client.render.vertex; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import javax.annotation.Nullable; public class VertexConsumerUtils { /** @@ -10,7 +12,7 @@ public class VertexConsumerUtils { * @param consumer the consumer to convert * @return a {@link VertexBufferWriter}, or null if the consumer does not support this */ - public static VertexBufferWriter convertOrLog(VertexConsumer consumer) { + public static @Nullable VertexBufferWriter convertOrLog(VertexConsumer consumer) { VertexBufferWriter writer = VertexBufferWriter.tryOf(consumer); if (writer == null) { @@ -19,4 +21,22 @@ public static VertexBufferWriter convertOrLog(VertexConsumer consumer) { return writer; } + + /** + * Attempt to convert a {@link VertexConsumer} into a {@link VertexBufferWriter} which can use optimized writes for + * the specified {@link VertexFormat}. If this fails, return null and log a message. + * + * @param consumer the consumer to convert + * @param format the vertex format that will be written + * @return a {@link VertexBufferWriter}, or null if the consumer cannot use optimized writes for the specified format + */ + public static @Nullable VertexBufferWriter convertOrLog(VertexConsumer consumer, VertexFormat format) { + VertexBufferWriter writer = VertexBufferWriter.tryOf(consumer, format); + + if (writer == null) { + VertexConsumerTracker.logBadConsumer(consumer); + } + + return writer; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/buffer/BufferBuilderExtension.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/buffer/BufferBuilderExtension.java index 40af81b01b..d780fef3fd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/buffer/BufferBuilderExtension.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/vertex/buffer/BufferBuilderExtension.java @@ -1,7 +1,18 @@ package net.caffeinemc.mods.sodium.client.render.vertex.buffer; +import com.mojang.blaze3d.vertex.VertexFormat; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; public interface BufferBuilderExtension extends VertexBufferWriter { void sodium$duplicateVertex(); + + /** + * @return the vertex format this buffer builder is currently configured to write + */ + VertexFormat sodium$getVertexFormat(); + + @Override + default boolean canUseIntrinsics(VertexFormat format) { + return this.sodium$getVertexFormat() == format; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java index be9521ebad..b6ea6eb10b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java @@ -6,6 +6,15 @@ import org.joml.Vector3d; public final class Viewport { + // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models + // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon + // to deal with floating point imprecision during a frustum check (see GH#2132). + public static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; + public static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + public static final float CHUNK_SECTION_NEARBY_MARGIN = 2.0f /* larger model extent */ + 0.125f /* epsilon */; + public static final float CHUNK_SECTION_PADDED_RADIUS = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; + private static final float LOOSER_MARGIN_EXTRA = CHUNK_SECTION_NEARBY_MARGIN - CHUNK_SECTION_MARGIN; + private final Frustum frustum; private final CameraTransform transform; @@ -25,19 +34,43 @@ public Viewport(Frustum frustum, Vector3d position) { this.blockCoords = BlockPos.containing(position.x, position.y, position.z); } - public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ, float floatSizeX, float floatSizeY, float floatSizeZ) { + public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ) { + float floatOriginX = (intOriginX - this.transform.intX) - this.transform.fracX; + float floatOriginY = (intOriginY - this.transform.intY) - this.transform.fracY; + float floatOriginZ = (intOriginZ - this.transform.intZ) - this.transform.fracZ; + + return this.frustum.testSection(floatOriginX, floatOriginY, floatOriginZ); + } + + public boolean isBoxVisibleLooser(int intOriginX, int intOriginY, int intOriginZ) { float floatOriginX = (intOriginX - this.transform.intX) - this.transform.fracX; float floatOriginY = (intOriginY - this.transform.intY) - this.transform.fracY; float floatOriginZ = (intOriginZ - this.transform.intZ) - this.transform.fracZ; + return this.frustum.testSectionExpanded(floatOriginX, floatOriginY, floatOriginZ, LOOSER_MARGIN_EXTRA); + } + + public boolean isBoxVisibleDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { return this.frustum.testAab( - floatOriginX - floatSizeX, - floatOriginY - floatSizeY, - floatOriginZ - floatSizeZ, + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, + + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize + ); + } + + public int getBoxIntersectionDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { + return this.frustum.intersectAab( + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, - floatOriginX + floatSizeX, - floatOriginY + floatSizeY, - floatOriginZ + floatSizeZ + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize ); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java index 3ec8d16aa6..91e06ebfe4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java @@ -2,4 +2,10 @@ public interface Frustum { boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); + + int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); + + boolean testSection(float floatOriginX, float floatOriginY, float floatOriginZ); + + boolean testSectionExpanded(float floatOriginX, float floatOriginY, float floatOriginZ, float v); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java index 88ff3b7738..196f2b10c0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java @@ -1,16 +1,115 @@ package net.caffeinemc.mods.sodium.client.render.viewport.frustum; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import org.joml.FrustumIntersection; +import org.joml.Vector4f; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; public final class SimpleFrustum implements Frustum { + private float nxX, nxY, nxZ, negNxW; + private float pxX, pxY, pxZ, negPxW; + private float nyX, nyY, nyZ, negNyW; + private float pyX, pyY, pyZ, negPyW; + private float nzX, nzY, nzZ, negNzW; + private float pzX, pzY, pzZ, negPzW; + private final FrustumIntersection frustum; + private static final MethodHandle PLANES_GETTER; + static { + try { + Field field = FrustumIntersection.class.getDeclaredField("planes"); + field.setAccessible(true); + PLANES_GETTER = MethodHandles.lookup().unreflectGetter(field); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to find planes field in JOML", e); + } + } + public SimpleFrustum(FrustumIntersection frustumIntersection) { this.frustum = frustumIntersection; + Vector4f[] planes; + try { + planes = (Vector4f[]) PLANES_GETTER.invokeExact(frustumIntersection); + } catch (Throwable e) { + throw new RuntimeException("Failed to access planes field in FrustumIntersection", e); + } + + this.nxX = planes[0].x; + this.nxY = planes[0].y; + this.nxZ = planes[0].z; + this.pxX = planes[1].x; + this.pxY = planes[1].y; + this.pxZ = planes[1].z; + this.nyX = planes[2].x; + this.nyY = planes[2].y; + this.nyZ = planes[2].z; + this.pyX = planes[3].x; + this.pyY = planes[3].y; + this.pyZ = planes[3].z; + this.nzX = planes[4].x; + this.nzY = planes[4].y; + this.nzZ = planes[4].z; + this.pzX = planes[5].x; + this.pzY = planes[5].y; + this.pzZ = planes[5].z; + + final float size = Viewport.CHUNK_SECTION_PADDED_RADIUS; + this.negNxW = -(planes[0].w + this.nxX * (this.nxX < 0 ? -size : size) + + this.nxY * (this.nxY < 0 ? -size : size) + + this.nxZ * (this.nxZ < 0 ? -size : size)); + this.negPxW = -(planes[1].w + this.pxX * (this.pxX < 0 ? -size : size) + + this.pxY * (this.pxY < 0 ? -size : size) + + this.pxZ * (this.pxZ < 0 ? -size : size)); + this.negNyW = -(planes[2].w + this.nyX * (this.nyX < 0 ? -size : size) + + this.nyY * (this.nyY < 0 ? -size : size) + + this.nyZ * (this.nyZ < 0 ? -size : size)); + this.negPyW = -(planes[3].w + this.pyX * (this.pyX < 0 ? -size : size) + + this.pyY * (this.pyY < 0 ? -size : size) + + this.pyZ * (this.pyZ < 0 ? -size : size)); + this.negNzW = -(planes[4].w + this.nzX * (this.nzX < 0 ? -size : size) + + this.nzY * (this.nzY < 0 ? -size : size) + + this.nzZ * (this.nzZ < 0 ? -size : size)); + this.negPzW = -(planes[5].w + this.pzX * (this.pzX < 0 ? -size : size) + + this.pzY * (this.pzY < 0 ? -size : size) + + this.pzZ * (this.pzZ < 0 ? -size : size)); + } + + public boolean testSection(float x, float y, float z) { + // Skip far plane checks because it has been ensured by searchDistance and isWithinRenderDistance check in OcclusionCuller + return this.nxX * x + this.nxY * y + this.nxZ * z >= this.negNxW && + this.pxX * x + this.pxY * y + this.pxZ * z >= this.negPxW && + this.nyX * x + this.nyY * y + this.nyZ * z >= this.negNyW && + this.pyX * x + this.pyY * y + this.pyZ * z >= this.negPyW && + this.nzX * x + this.nzY * y + this.nzZ * z >= this.negNzW; + } + + public boolean testSectionExpanded(float floatOriginX, float floatOriginY, float floatOriginZ, float extend) { + float minX = floatOriginX - extend; + float maxX = floatOriginX + extend; + float minY = floatOriginY - extend; + float maxY = floatOriginY + extend; + float minZ = floatOriginZ - extend; + float maxZ = floatOriginZ + extend; + + return this.nxX * (this.nxX < 0 ? minX : maxX) + this.nxY * (this.nxY < 0 ? minY : maxY) + this.nxZ * (this.nxZ < 0 ? minZ : maxZ) >= this.negNxW && + this.pxX * (this.pxX < 0 ? minX : maxX) + this.pxY * (this.pxY < 0 ? minY : maxY) + this.pxZ * (this.pxZ < 0 ? minZ : maxZ) >= this.negPxW && + this.nyX * (this.nyX < 0 ? minX : maxX) + this.nyY * (this.nyY < 0 ? minY : maxY) + this.nyZ * (this.nyZ < 0 ? minZ : maxZ) >= this.negNyW && + this.pyX * (this.pyX < 0 ? minX : maxX) + this.pyY * (this.pyY < 0 ? minY : maxY) + this.pyZ * (this.pyZ < 0 ? minZ : maxZ) >= this.negPyW && + this.nzX * (this.nzX < 0 ? minX : maxX) + this.nzY * (this.nzY < 0 ? minY : maxY) + this.nzZ * (this.nzZ < 0 ? minZ : maxZ) >= this.negNzW && + this.pzX * (this.pzX < 0 ? minX : maxX) + this.pzY * (this.pzY < 0 ? minY : maxY) + this.pzZ * (this.pzZ < 0 ? minZ : maxZ) >= this.negPzW; } @Override public boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { return this.frustum.testAab(minX, minY, minZ, maxX, maxY, maxZ); } + + @Override + public int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return this.frustum.intersectAab(minX, minY, minZ, maxX, maxY, maxZ); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformBlockAccess.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformBlockAccess.java index 7679b20dc3..69a58a3304 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformBlockAccess.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformBlockAccess.java @@ -2,7 +2,6 @@ import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; import net.caffeinemc.mods.sodium.client.render.frapi.render.AmbientOcclusionMode; -import net.fabricmc.fabric.api.util.TriState; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.resources.model.BakedModel; @@ -13,7 +12,6 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; -import org.jetbrains.annotations.Nullable; public interface PlatformBlockAccess { PlatformBlockAccess INSTANCE = Services.load(PlatformBlockAccess.class); @@ -85,4 +83,13 @@ static PlatformBlockAccess getInstance() { * @return Whether this block entity should activate the outline shader. */ boolean shouldBlockEntityGlow(BlockEntity blockEntity, LocalPlayer player); + + /** + * Determines if a fluid adjacent to the block on the given side should not be rendered. + * + * @param adjDirection the face of this block that the fluid is adjacent to + * @param fluid the fluid that is touching that face + * @return if this block should cause the fluid's face to not render + */ + boolean shouldOccludeFluid(Direction adjDirection, BlockState adjBlockState, FluidState fluid); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformModelAccess.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformModelAccess.java index efca9b4d44..af68c4f700 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformModelAccess.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformModelAccess.java @@ -9,7 +9,6 @@ import net.minecraft.core.SectionPos; import net.minecraft.util.RandomSource; import net.minecraft.world.level.BlockAndTintGetter; -import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.ApiStatus; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformRuntimeInformation.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformRuntimeInformation.java index e3326ae6e9..1bed9f7399 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformRuntimeInformation.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/PlatformRuntimeInformation.java @@ -29,7 +29,7 @@ static PlatformRuntimeInformation getInstance() { Path getConfigDirectory(); /** - * Returns if the platform has a early loading screen. + * Returns if the platform has an early loading screen. */ boolean platformHasEarlyLoadingScreen(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/SodiumModelDataContainer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/SodiumModelDataContainer.java index acd20d8f67..a3c592a40b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/services/SodiumModelDataContainer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/services/SodiumModelDataContainer.java @@ -3,8 +3,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import net.minecraft.core.BlockPos; -import java.util.Map; - /** * A container that holds the platform's model data. */ @@ -18,10 +16,10 @@ public SodiumModelDataContainer(Long2ObjectMap modelDataMap) { } public SodiumModelData getModelData(BlockPos pos) { - return modelDataMap.getOrDefault(pos.asLong(), SodiumModelData.EMPTY); + return this.modelDataMap.getOrDefault(pos.asLong(), SodiumModelData.EMPTY); } public boolean isEmpty() { - return isEmpty; + return this.isEmpty; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Dim2i.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Dim2i.java index 2ce45dd3c6..809ecbb6c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Dim2i.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Dim2i.java @@ -20,4 +20,32 @@ public int getCenterX() { public int getCenterY() { return this.y + (this.height / 2); } + + public Dim2i inset(int left, int right, int top, int bottom) { + return new Dim2i(this.x + left, this.y + top, this.width - left - right, this.height - top - bottom); + } + + public Dim2i insetX(int amount) { + return this.inset(amount, amount, 0, 0); + } + + public Dim2i insetY(int amount) { + return this.inset(0, 0, amount, amount); + } + + public Dim2i insetLeft(int amount) { + return this.inset(amount, 0, 0, 0); + } + + public Dim2i insetRight(int amount) { + return this.inset(0, amount, 0, 0); + } + + public Dim2i insetTop(int amount) { + return this.inset(0, 0, amount, 0); + } + + public Dim2i insetBottom(int amount) { + return this.inset(0, 0, 0, amount); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FlawlessFrames.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FlawlessFrames.java index 9202878d83..44ecde8e24 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FlawlessFrames.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FlawlessFrames.java @@ -13,7 +13,7 @@ *

* In Sodium's case, this means waiting for all chunks to be fully updated and ready for rendering before each frame. *

- * See https://github.com/grondag/frex/pull/9 + * See this PR */ public class FlawlessFrames { private static final Set ACTIVE = Collections.newSetFromMap(new ConcurrentHashMap<>()); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FrameTimeStatistics.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FrameTimeStatistics.java new file mode 100644 index 0000000000..95f283632e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/FrameTimeStatistics.java @@ -0,0 +1,147 @@ +package net.caffeinemc.mods.sodium.client.util; + +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongComparators; +import it.unimi.dsi.fastutil.longs.LongHeaps; +import it.unimi.dsi.fastutil.objects.Reference2LongArrayMap; + +import java.util.Arrays; +import java.util.Comparator; + +// Maintains a ring buffer of frame durations populated from DebugScreenOverlay#logFrameDuration. We keep our own buffer because vanilla's FpsAccumulator only retains 240 samples, which gives only a fractional sample in the p99.9 tail. +public final class FrameTimeStatistics { + private static final int SAMPLE_COUNT = 1000; + + public record Percentile(String name, int window, float p) { + } + + private static final Percentile[] PERCENTILES = new Percentile[] { + new Percentile("p50", 200, 0.5f), + new Percentile("p98", Integer.MAX_VALUE, 0.98f), + new Percentile("p99.5",Integer.MAX_VALUE, 0.995f) + }; + + // PERCENTILES sorted (window asc, p desc) so each step of compute() either keeps the heap window or extends it with older sample, and within an equal-window run popDownTo only shrinks the heap. + private static final Percentile[] CALCULATION_ORDER = computeCalculationOrder(); + + public static final FrameTimeStatistics INSTANCE = new FrameTimeStatistics(); + + private static final LongComparator HEAP_COMPARATOR = LongComparators.OPPOSITE_COMPARATOR; + + private final long[] samples = new long[SAMPLE_COUNT]; + private int writeIndex = 0; + private int sampleSize = 0; + + private long[] heap; + private volatile Reference2LongArrayMap cached; + + private FrameTimeStatistics() { + } + + public Reference2LongArrayMap get() { + if (this.cached == null) { + this.cached = this.compute(); + } + return this.cached; + } + + // called from the render thread for every frame whose duration vanilla logs + public void logSample(long frameDuration) { + this.samples[this.writeIndex] = frameDuration; + this.writeIndex = (this.writeIndex + 1) % SAMPLE_COUNT; + if (this.sampleSize < SAMPLE_COUNT) { + this.sampleSize++; + } + } + + public void invalidate() { + this.cached = null; + } + + private Reference2LongArrayMap compute() { + int n = this.sampleSize; + if (n <= 0) { + return null; + } + + if (this.heap == null) { + this.heap = new long[SAMPLE_COUNT]; + } + var heap = this.heap; + + // pre-populate so iteration order in the result map matches the display order in PERCENTILES + var results = new Reference2LongArrayMap(PERCENTILES.length); + for (Percentile percentile : PERCENTILES) { + results.put(percentile, -1L); + } + + // currentWindow is how many samples we've copied from the ring buffer into our calculation array so far + int currentWindow = 0; + int heapSize = 0; + for (var percentile : CALCULATION_ORDER) { + int window = Math.min(percentile.window(), n); + + if (window > currentWindow) { + // append the next-older block of samples and re-heapify the full window; popped entries from the previous group rejoin the heap automatically + this.copyMostRecentSamples(heap, currentWindow, window - currentWindow); + LongHeaps.makeHeap(heap, window, HEAP_COMPARATOR); + currentWindow = window; + heapSize = window; + } + + heapSize = popDownTo(heap, heapSize, window - rankFromTop(percentile.p(), window)); + results.put(percentile, heap[0]); + } + return results; + } + + // copy ring-buffer samples into dest[destOffset..destOffset + count), where destOffset is the index of the first sample expressed as "k-th most recent" (0 = newest). One arraycopy if the source range doesn't wrap, two if it does. The order within dest is irrelevant because the consumer rebuilds the heap. + private void copyMostRecentSamples(long[] dest, int destOffset, int count) { + if (count <= 0) { + return; + } + int srcStart = Math.floorMod(this.writeIndex - destOffset - count, SAMPLE_COUNT); + int firstChunk = Math.min(count, SAMPLE_COUNT - srcStart); + System.arraycopy(this.samples, srcStart, dest, destOffset, firstChunk); + if (firstChunk < count) { + System.arraycopy(this.samples, 0, dest, destOffset + firstChunk, count - firstChunk); + } + } + + private static Percentile[] computeCalculationOrder() { + var order = Arrays.copyOf(PERCENTILES, PERCENTILES.length); + + // sort by window ascending, p descending + var byWindowThenPDesc = Comparator + .comparingInt(Percentile::window) + .thenComparing(Percentile::p, Comparator.reverseOrder()); + + Arrays.sort(order, byWindowThenPDesc); + + return order; + } + + // index of the p-quantile in a descending sequence of length n + private static int rankFromTop(double p, int n) { + int rank = (int) Math.floor((1.0 - p) * n); + if (rank < 0) { + return 0; + } + if (rank >= n) { + return n - 1; + } + return rank; + } + + // pop maxes (swap with tail, sift down) until the heap shrinks to targetSize + private static int popDownTo(long[] heap, int heapSize, int targetSize) { + while (heapSize > targetSize) { + heapSize--; + heap[0] = heap[heapSize]; + if (heapSize > 0) { + LongHeaps.downHeap(heap, heapSize, 0, HEAP_COMPARATOR); + } + } + return heapSize; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Int2.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Int2.java new file mode 100644 index 0000000000..a0198495ab --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/Int2.java @@ -0,0 +1,13 @@ +package net.caffeinemc.mods.sodium.client.util; + +import java.nio.ByteOrder; + +public class Int2 { + public static long pack(int a, int b) { + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + return ((a & 0xFFFFFFFFL) << 0) | ((b & 0xFFFFFFFFL) << 32); + } else { + return ((a & 0xFFFFFFFFL) << 32) | ((b & 0xFFFFFFFFL) << 0); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java index 5a11ae1355..c0d2e25d9f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java @@ -1,5 +1,11 @@ package net.caffeinemc.mods.sodium.client.util; + +import org.joml.Vector3dc; +import org.joml.Vector3fc; + +import static org.joml.Math.fma; + public class MathUtil { /** * @return True if the specified number is greater than zero and is a power of two, otherwise false @@ -12,6 +18,20 @@ public static long toMib(long bytes) { return bytes / (1024L * 1024L); // 1 MiB = 1048576 (2^20) bytes } + /** + *

Rounds the integer {@param num} up to the next multiple of {@param alignment}. This multiple *MUST* be + * a power-of-two, or undefined behavior will occur.

+ * + * @param num The number to round up + * @param alignment The power-of-two multiple to round to + * @return The rounded number + */ + public static int align(int num, int alignment) { + int additive = alignment - 1; + int mask = ~additive; + return (num + additive) & mask; + } + /** * Converts a float to a comparable integer value. This is used to compare * floating point values by their int bits (for example packed in a long). @@ -29,4 +49,20 @@ public static int floatToComparableInt(float f) { public static float comparableIntToFloat(int i) { return Float.intBitsToFloat(i ^ ((i >> 31) & 0x7FFFFFFF)); } + + public static double exponentialMovingAverage(double oldValue, double newValue, double newValueContribution) { + return newValueContribution * newValue + (1 - newValueContribution) * oldValue; + } + + public static long exponentialMovingAverage(long oldValue, long newValue, float newValueContribution) { + return (long) (newValueContribution * newValue) + (long) ((1 - newValueContribution) * oldValue); + } + + public static double floatDoubleDot(Vector3fc a, Vector3dc b) { + return fma(a.x(), b.x(), fma(a.y(), b.y(), a.z() * b.z())); + } + + public static double floatDoubleDot(Vector3fc a, double bx, double by, double bz) { + return fma(a.x(), bx, fma(a.y(), by, a.z() * bz)); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/ModelQuadUtil.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/ModelQuadUtil.java index a2cd31a62d..400a5856cb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/ModelQuadUtil.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/ModelQuadUtil.java @@ -3,10 +3,10 @@ /** * 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 diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/NativeImageHelper.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/NativeImageHelper.java index 2c029e8e32..405279370d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/NativeImageHelper.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/NativeImageHelper.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.util; -import net.caffeinemc.mods.sodium.mixin.features.textures.NativeImageAccessor; import com.mojang.blaze3d.platform.NativeImage; +import net.caffeinemc.mods.sodium.mixin.features.textures.NativeImageAccessor; import java.util.Locale; @@ -13,6 +13,6 @@ public static long getPointerRGBA(NativeImage nativeImage) { } return ((NativeImageAccessor) (Object) nativeImage) // duck type since NativeImage is final - .getPixels(); + .sodium$getPixels(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java index 2fb9d6252a..ed7b08edd4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java @@ -1,39 +1,25 @@ package net.caffeinemc.mods.sodium.client.util.collections; +import net.caffeinemc.mods.sodium.client.util.MathUtil; + import java.util.Arrays; /** - * Originally authored here: https://github.com/CaffeineMC/sodium/blob/ddfb9f21a54bfb30aa876678204371e94d8001db/src/main/java/net/caffeinemc/sodium/util/collections/BitArray.java + * Originally authored here * @author burgerindividual */ public class BitArray { private static final int ADDRESS_BITS_PER_WORD = 6; private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; private static final int BIT_INDEX_MASK = BITS_PER_WORD - 1; - private static final long WORD_MASK = 0xFFFFFFFFFFFFFFFFL; + private static final long WORD_MASK = -1L; private final long[] words; - private final int count; - - /** - * Returns {@param num} aligned to the next multiple of {@param alignment}. - * - * Taken from https://github.com/CaffeineMC/sodium/blob/1.19.x/next/components/gfx-utils/src/main/java/net/caffeinemc/gfx/util/misc/MathUtil.java - * - * @param num The number that will be rounded if needed - * @param alignment The multiple that the output will be rounded to (must be a - * power-of-two) - * @return The aligned position, either equal to or greater than {@param num} - */ - private static int align(int num, int alignment) { - int additive = alignment - 1; - int mask = ~additive; - return (num + additive) & mask; - } + private final int capacity; - public BitArray(int count) { - this.words = new long[(align(count, BITS_PER_WORD) >> ADDRESS_BITS_PER_WORD)]; - this.count = count; + public BitArray(int capacity) { + this.words = new long[(MathUtil.align(capacity, BITS_PER_WORD) >> ADDRESS_BITS_PER_WORD)]; + this.capacity = capacity; } public boolean get(int index) { @@ -64,13 +50,14 @@ public void set(int startIdx, int endIdx) { long firstWordMask = WORD_MASK << startIdx; long lastWordMask = WORD_MASK >>> -endIdx; + if (startWordIndex == endWordIndex) { this.words[startWordIndex] |= (firstWordMask & lastWordMask); } else { this.words[startWordIndex] |= firstWordMask; for (int i = startWordIndex + 1; i < endWordIndex; i++) { - this.words[i] = 0xFFFFFFFFFFFFFFFFL; + this.words[i] = -1L; } this.words[endWordIndex] |= lastWordMask; @@ -86,128 +73,33 @@ public void unset(int startIdx, int endIdx) { long firstWordMask = ~(WORD_MASK << startIdx); long lastWordMask = ~(WORD_MASK >>> -endIdx); + if (startWordIndex == endWordIndex) { this.words[startWordIndex] &= (firstWordMask & lastWordMask); } else { this.words[startWordIndex] &= firstWordMask; for (int i = startWordIndex + 1; i < endWordIndex; i++) { - this.words[i] = 0x0000000000000000L; + this.words[i] = 0L; } this.words[endWordIndex] &= lastWordMask; } } - // FIXME - /* public boolean checkUnset(int startIdx, int endIdx) { - int startWordIndex = wordIndex(startIdx); - int endWordIndex = wordIndex(endIdx - 1); - - long firstWordMask = ~(WORD_MASK << startIdx); - long lastWordMask = ~(WORD_MASK >>> -endIdx); - if (startWordIndex == endWordIndex) { - return (this.words[startWordIndex] & firstWordMask & lastWordMask) == 0x0000000000000000L; - } else { - if ((this.words[startWordIndex] & firstWordMask) != 0x0000000000000000L) { - return false; - } - - for (int i = startWordIndex + 1; i < endWordIndex; i++) { - if (this.words[i] != 0x0000000000000000L) { - return false; - } - } - - return (this.words[endWordIndex] & lastWordMask) == 0x0000000000000000L; - } - }*/ - - public void copy(BitArray src, int startIdx, int endIdx) { - int startWordIndex = wordIndex(startIdx); - int endWordIndex = wordIndex(endIdx - 1); - - long firstWordMask = WORD_MASK << startIdx; - long lastWordMask = WORD_MASK >>> -endIdx; - if (startWordIndex == endWordIndex) { - long combinedMask = firstWordMask & lastWordMask; - long invCombinedMask = ~combinedMask; - this.words[startWordIndex] = (this.words[startWordIndex] & invCombinedMask) - | (src.words[startWordIndex] & combinedMask); - } else { - long invFirstWordMask = ~firstWordMask; - long invLastWordMask = ~lastWordMask; - - this.words[startWordIndex] = (this.words[startWordIndex] & invFirstWordMask) - | (src.words[startWordIndex] & firstWordMask); - - int length = endWordIndex - (startWordIndex + 1); - if (length > 0) { - System.arraycopy( - src.words, - startWordIndex + 1, - this.words, - startWordIndex + 1, - length); - } - - this.words[endWordIndex] = (this.words[endWordIndex] & invLastWordMask) - | (src.words[endWordIndex] & lastWordMask); - } - } - - public void copy(BitArray src, int index) { - int wordIndex = wordIndex(index); - long invBitMask = 1L << bitIndex(index); - long bitMask = ~invBitMask; - this.words[wordIndex] = (this.words[wordIndex] & bitMask) | (src.words[wordIndex] & invBitMask); - } - - public void and(BitArray src, int startIdx, int endIdx) { - int startWordIndex = wordIndex(startIdx); - int endWordIndex = wordIndex(endIdx - 1); - - long firstWordMask = WORD_MASK << startIdx; - long lastWordMask = WORD_MASK >>> -endIdx; - if (startWordIndex == endWordIndex) { - long combinedMask = firstWordMask & lastWordMask; - long invCombinedMask = ~combinedMask; - this.words[startWordIndex] &= (src.words[startWordIndex] | invCombinedMask); - } else { - long invFirstWordMask = ~firstWordMask; - long invLastWordMask = ~lastWordMask; - - this.words[startWordIndex] &= (src.words[startWordIndex] | invFirstWordMask); - - for (int i = startWordIndex + 1; i < endWordIndex; i++) { - this.words[i] &= src.words[i]; - } - - this.words[endWordIndex] &= (src.words[endWordIndex] | invLastWordMask); - } - } - - private static int wordIndex(int index) { - return index >> ADDRESS_BITS_PER_WORD; - } - - private static int bitIndex(int index) { - return index & BIT_INDEX_MASK; - } - public void fill(boolean value) { - Arrays.fill(this.words, value ? 0xFFFFFFFFFFFFFFFFL : 0x0000000000000000L); + Arrays.fill(this.words, value ? -1L : 0L); } - public void unset() { + public void unsetAll() { this.fill(false); } - public void set() { + public void setAll() { this.fill(true); } - public int count() { + public int countSetBits() { int sum = 0; for (long word : this.words) { @@ -218,7 +110,7 @@ public int count() { } public int capacity() { - return this.count; + return this.capacity; } public boolean getAndSet(int index) { @@ -262,13 +154,11 @@ public int nextSetBit(int fromIndex) { } } - public String toBitString() { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < this.count; i++) { - sb.append(this.get(i) ? '1' : '0'); - } + private static int wordIndex(int index) { + return index >> ADDRESS_BITS_PER_WORD; + } - return sb.toString(); + private static int bitIndex(int index) { + return index & BIT_INDEX_MASK; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/DoubleBufferedQueue.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/DoubleBufferedQueue.java index 605623eb66..25393b70eb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/DoubleBufferedQueue.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/DoubleBufferedQueue.java @@ -109,5 +109,10 @@ private void resize(int length) { private static int getNextSize(int minimumSize, int currentSize) { return Math.max(minimumSize, currentSize << 1); } + + @Override + public boolean isEmpty() { + return this.readIndex == this.writeIndex; + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/WriteQueue.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/WriteQueue.java index 061ebd16cc..7f71903faa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/WriteQueue.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/WriteQueue.java @@ -6,4 +6,6 @@ public interface WriteQueue { void ensureCapacity(int numElements); void enqueue(@NotNull E e); + + boolean isEmpty(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/color/BoxBlur.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/color/BoxBlur.java index f3b2d70640..e0ac2586fe 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/color/BoxBlur.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/color/BoxBlur.java @@ -4,67 +4,63 @@ import net.minecraft.util.Mth; public class BoxBlur { - - public static void blur(ColorBuffer buf, ColorBuffer tmp, int radius) { - if (buf.width != tmp.width || buf.height != tmp.height) { - throw new IllegalArgumentException("Color buffers must have same dimensions"); - } - - if (isHomogenous(buf.data)) { + public static void blur(int[] src, int[] tmp, int width, int height, int radius) { + if (isHomogenous(src)) { return; } - blurImpl(buf.data, tmp.data, buf.width, buf.height, radius); // X-axis - blurImpl(tmp.data, buf.data, buf.width, buf.height, radius); // Y-axis + blurImpl(src, tmp, radius, width - radius, width, 0, height, height, radius); // X-axis + blurImpl(tmp, src, radius, width - radius, width, radius, height - radius, height, radius); // Y-axis } - private static void blurImpl(int[] src, int[] dst, int width, int height, int radius) { - int multiplier = getAveragingMultiplier((radius * 2) + 1); - - for (int y = 0; y < height; y++) { - int srcRowOffset = ColorBuffer.getIndex(0, y, width); - - int red, green, blue; - - { - int color = src[srcRowOffset]; - red = ColorARGB.unpackRed(color); - green = ColorARGB.unpackGreen(color); - blue = ColorARGB.unpackBlue(color); + private static void blurImpl(int[] src, int[] dst, int x0, int x1, int width, int y0, int y1, int height, int radius) { + int windowSize = (radius * 2) + 1; + int multiplier = getAveragingMultiplier(windowSize); + + for (int y = y0; y < y1; y++) { + int accR = 0; + int accG = 0; + int accB = 0; + + int windowPivotIndex = ColorBuffer.getIndex(x0, y, width); + int windowTailIndex = windowPivotIndex - radius; + int windowHeadIndex = windowPivotIndex + radius; + + // Initialize window + for (int x = -radius; x <= radius; x++) { + var color = src[windowPivotIndex + x]; + accR += ColorARGB.unpackRed(color); + accG += ColorARGB.unpackGreen(color); + accB += ColorARGB.unpackBlue(color); } - // Extend the window backwards by repeating the colors at the edge N times - red += red * radius; - green += green * radius; - blue += blue * radius; - - // Extend the window forwards by sampling ahead N times - for (int x = 1; x <= radius; x++) { - var color = src[srcRowOffset + x]; - red += ColorARGB.unpackRed(color); - green += ColorARGB.unpackGreen(color); - blue += ColorARGB.unpackBlue(color); - } + // Scan forwards + int x = x0; - for (int x = 0; x < width; x++) { + while (true) { // The x and y coordinates are transposed to flip the output image - dst[ColorBuffer.getIndex(y, x, width)] = averageRGB(red, green, blue, multiplier); + // noinspection SuspiciousNameCombination + dst[ColorBuffer.getIndex(y, x, width)] = averageRGB(accR, accG, accB, multiplier); + x++; + + if (x >= x1) { + break; + } { // Remove the color values that are behind the window - var color = src[srcRowOffset + Math.max(0, x - radius)]; - - red -= ColorARGB.unpackRed(color); - green -= ColorARGB.unpackGreen(color); - blue -= ColorARGB.unpackBlue(color); + var color = src[windowTailIndex++]; + accR -= ColorARGB.unpackRed(color); + accG -= ColorARGB.unpackGreen(color); + accB -= ColorARGB.unpackBlue(color); } { // Add the color values that are ahead of the window - var color = src[srcRowOffset + Math.min(width - 1, x + radius + 1)]; - red += ColorARGB.unpackRed(color); - green += ColorARGB.unpackGreen(color); - blue += ColorARGB.unpackBlue(color); + var color = src[++windowHeadIndex]; + accR += ColorARGB.unpackRed(color); + accG += ColorARGB.unpackGreen(color); + accB += ColorARGB.unpackBlue(color); } } } @@ -108,7 +104,7 @@ private static boolean isHomogenous(int[] array) { } public static class ColorBuffer { - protected final int[] data; + public final int[] data; protected final int width, height; public ColorBuffer(int width, int height) { @@ -121,13 +117,12 @@ public void set(int x, int y, int color) { this.data[getIndex(x, y, this.width)] = color; } - public int get(int x, int y) { return this.data[getIndex(x, y, this.width)]; } public static int getIndex(int x, int y, int width) { - return (y * width) + x; + return x + (y * width); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/DoubleInterval.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/DoubleInterval.java index e5c60f3852..1d04236853 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/DoubleInterval.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/DoubleInterval.java @@ -92,14 +92,14 @@ protected Interval create() { */ @Override public boolean isEmpty() { - if (getStart() != null && getStart().isNaN()) + if (this.getStart() != null && this.getStart().isNaN()) return true; - if (getEnd() != null && getEnd().isNaN()) + if (this.getEnd() != null && this.getEnd().isNaN()) return true; - if (getStart() != null && getEnd() != null) { - if (getStart() == Double.POSITIVE_INFINITY && getEnd() == Double.POSITIVE_INFINITY) + if (this.getStart() != null && this.getEnd() != null) { + if (this.getStart() == Double.POSITIVE_INFINITY && this.getEnd() == Double.POSITIVE_INFINITY) return true; - if (getStart() == Double.NEGATIVE_INFINITY && getEnd() == Double.NEGATIVE_INFINITY) + if (this.getStart() == Double.NEGATIVE_INFINITY && this.getEnd() == Double.NEGATIVE_INFINITY) return true; } return super.isEmpty(); @@ -123,24 +123,24 @@ public boolean isEmpty() { */ @Override public Double getMidpoint() { - if (isEmpty()) + if (this.isEmpty()) return null; // Handle null values - if (getStart() == null && getEnd() == null) + if (this.getStart() == null && this.getEnd() == null) return 0.0; - if (getStart() == null) - return getEnd() - OFFSET; - if (getEnd() == null) - return getStart() + OFFSET; + if (this.getStart() == null) + return this.getEnd() - OFFSET; + if (this.getEnd() == null) + return this.getStart() + OFFSET; // Now we are sure there are no more null values involved - if (getStart() == Double.NEGATIVE_INFINITY && getEnd() == Double.POSITIVE_INFINITY) + if (this.getStart() == Double.NEGATIVE_INFINITY && this.getEnd() == Double.POSITIVE_INFINITY) return 0.0; - if (getStart() == Double.NEGATIVE_INFINITY) - return getEnd() - OFFSET; - if (getEnd() == Double.POSITIVE_INFINITY) - return getStart() + OFFSET; - return getStart() + (getEnd() - getStart()) / 2; + if (this.getStart() == Double.NEGATIVE_INFINITY) + return this.getEnd() - OFFSET; + if (this.getEnd() == Double.POSITIVE_INFINITY) + return this.getStart() + OFFSET; + return this.getStart() + (this.getEnd() - this.getStart()) / 2; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/Interval.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/Interval.java index 869bb7917e..3ca8ed67cd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/Interval.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/Interval.java @@ -244,7 +244,7 @@ public boolean isEmpty() { * @return The newly created interval. */ protected Interval create(T start, boolean isStartInclusive, T end, boolean isEndInclusive) { - Interval interval = create(); + Interval interval = this.create(); interval.start = start; interval.isStartInclusive = isStartInclusive; interval.end = end; @@ -287,7 +287,7 @@ public boolean isEndInclusive() { * @return {@code true}, if the current interval contains the {@code query} point or false otherwise. */ public boolean contains(T query) { - if (isEmpty() || query == null) { + if (this.isEmpty() || query == null) { return false; } @@ -308,7 +308,7 @@ public boolean contains(T query) { * @return The intersection of the current interval wih the {@code other} interval. */ public Interval getIntersection(Interval other) { - if (other == null || isEmpty() || other.isEmpty()) + if (other == null || this.isEmpty() || other.isEmpty()) return null; // Make sure that the one with the smaller starting point gets intersected with the other. // If necessary, swap the intervals @@ -352,7 +352,7 @@ public Interval getIntersection(Interval other) { isNewEndInclusive = other.isEndInclusive; } } - Interval intersection = create(newStart, isNewStartInclusive, newEnd, isNewEndInclusive); + Interval intersection = this.create(newStart, isNewStartInclusive, newEnd, isNewEndInclusive); return intersection.isEmpty() ? null : intersection; } @@ -368,7 +368,7 @@ public Interval getIntersection(Interval other) { public boolean intersects(Interval query) { if (query == null) return false; - Interval intersection = getIntersection(query); + Interval intersection = this.getIntersection(query); return intersection != null; } @@ -391,7 +391,7 @@ public boolean isRightOf(T point, boolean inclusive) { int compare = point.compareTo(this.start); if (compare != 0) return compare < 0; - return !isStartInclusive() || !inclusive; + return !this.isStartInclusive() || !inclusive; } /** @@ -406,7 +406,7 @@ public boolean isRightOf(T point, boolean inclusive) { * interval, or {@code false} instead. */ public boolean isRightOf(T point) { - return isRightOf(point, true); + return this.isRightOf(point, true); } /** @@ -424,7 +424,7 @@ public boolean isRightOf(T point) { public boolean isRightOf(Interval other) { if (other == null || other.isEmpty()) return false; - return isRightOf(other.end, other.isEndInclusive()); + return this.isRightOf(other.end, other.isEndInclusive()); } /** @@ -446,7 +446,7 @@ public boolean isLeftOf(T point, boolean inclusive) { int compare = point.compareTo(this.end); if (compare != 0) return compare > 0; - return !isEndInclusive() || !inclusive; + return !this.isEndInclusive() || !inclusive; } /** @@ -461,7 +461,7 @@ public boolean isLeftOf(T point, boolean inclusive) { * interval, or {@code false} instead. */ public boolean isLeftOf(T point) { - return isLeftOf(point, true); + return this.isLeftOf(point, true); } /** @@ -479,7 +479,7 @@ public boolean isLeftOf(T point) { public boolean isLeftOf(Interval other) { if (other == null || other.isEmpty()) return false; - return isLeftOf(other.start, other.isStartInclusive()); + return this.isLeftOf(other.start, other.isStartInclusive()); } /** @@ -533,7 +533,7 @@ private int compareEnds(Interval other) { *

*

* To ensure that this comparator can also be used in sets it considers the end points of the intervals, if the - * start points are the same. Otherwise the set will not be able to handle two different intervals, sharing + * start points are the same. Otherwise, the set will not be able to handle two different intervals, sharing * the same starting point, and omit one of the intervals. *

*

@@ -562,7 +562,7 @@ private int compareEnds(Interval other) { *

*

* To ensure that this comparator can also be used in sets it considers the start points of the intervals, if the - * end points are the same. Otherwise the set will not be able to handle two different intervals, sharing + * end points are the same. Otherwise, the set will not be able to handle two different intervals, sharing * the same end point, and omit one of the intervals. *

*

diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/TreeNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/TreeNode.java index 6845489a1c..cd4c7b7b91 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/TreeNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/interval_tree/TreeNode.java @@ -224,17 +224,17 @@ private TreeNode balanceOut() { // The tree is right-heavy. if (height(this.right.left) > height(this.right.right)) { this.right = this.right.rightRotate(); - return leftRotate(); + return this.leftRotate(); } else { - return leftRotate(); + return this.leftRotate(); } } else if (balance > 1) { // The tree is left-heavy. if (height(this.left.right) > height(this.left.left)) { this.left = this.left.leftRotate(); - return rightRotate(); + return this.rightRotate(); } else - return rightRotate(); + return this.rightRotate(); } else { // The tree is already balanced. return this; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/iterator/WrappedIterator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/iterator/WrappedIterator.java new file mode 100644 index 0000000000..02543ffdfb --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/iterator/WrappedIterator.java @@ -0,0 +1,39 @@ +package net.caffeinemc.mods.sodium.client.util.iterator; + +import java.util.Iterator; + +public class WrappedIterator implements Iterator { + private final Iterator delegate; + + private WrappedIterator(Iterator delegate) { + this.delegate = delegate; + } + + public static WrappedIterator create(Iterable iterable) { + return new WrappedIterator<>(iterable.iterator()); + } + + @Override + public boolean hasNext() { + try { + return this.delegate.hasNext(); + } catch (Throwable t) { + throw new Exception("Iterator#hasNext() threw unhandled exception", t); + } + } + + @Override + public T next() { + try { + return this.delegate.next(); + } catch (Throwable t) { + throw new Exception("Iterator#next() threw unhandled exception", t); + } + } + + public static class Exception extends RuntimeException { + private Exception(String message, Throwable t) { + super(message, t); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/AbstractSort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/AbstractSort.java deleted file mode 100644 index 93fd652f6e..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/AbstractSort.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.caffeinemc.mods.sodium.client.util.sorting; - - -public class AbstractSort { - protected static int[] createIndexBuffer(int length) { - var indices = new int[length]; - - for (int i = 0; i < length; i++) { - indices[i] = i; - } - - return indices; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/InsertionSort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/InsertionSort.java deleted file mode 100644 index 08dfbc4ef9..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/InsertionSort.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.caffeinemc.mods.sodium.client.util.sorting; - -public class InsertionSort extends AbstractSort { - public static void insertionSort(final int[] indices, final int fromIndex, final int toIndex, final float[] keys) { - int index = fromIndex; - - while (++index < toIndex) { - int t = indices[index]; - int j = index; - - int u = indices[j - 1]; - - while (keys[u] < keys[t]) { - indices[j] = u; - - if (fromIndex == j - 1) { - --j; - break; - } - - u = indices[--j - 1]; - } - - indices[j] = t; - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/MergeSort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/MergeSort.java deleted file mode 100644 index 5083da0274..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/MergeSort.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2002-2017 Sebastiano Vigna - * - * 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. - * - * - * - * For the sorting and binary search code: - * - * Copyright (C) 1999 CERN - European Organization for Nuclear Research. - * - * Permission to use, copy, modify, distribute and sell this software and - * its documentation for any purpose is hereby granted without fee, - * provided that the above copyright notice appear in all copies and that - * both that copyright notice and this permission notice appear in - * supporting documentation. CERN makes no representations about the - * suitability of this software for any purpose. It is provided "as is" - * without expressed or implied warranty. - */ -package net.caffeinemc.mods.sodium.client.util.sorting; - - -/** - * Based upon {@link it.unimi.dsi.fastutil.ints.IntArrays} implementation, but it eliminates the use of a user-supplied - * function and instead sorts an array of floats directly. This helps to improve runtime performance. - */ -public class MergeSort extends AbstractSort { - private static final int INSERTION_SORT_THRESHOLD = 16; - - public static int[] mergeSort(float[] keys) { - var indices = createIndexBuffer(keys.length); - mergeSort(indices, keys); - - return indices; - } - - private static void mergeSort(final int[] indices, final float[] keys) { - mergeSort(indices, keys, 0, indices.length, null); - } - - private static void mergeSort(final int[] indices, final float[] keys, final int fromIndex, final int toIndex, int[] supp) { - int len = toIndex - fromIndex; - - // Insertion sort on smallest arrays - if (len < INSERTION_SORT_THRESHOLD) { - InsertionSort.insertionSort(indices, fromIndex, toIndex, keys); - return; - } - - if (supp == null) { - supp = indices.clone(); - } - - // Recursively sort halves of a into supp - final int mid = (fromIndex + toIndex) >>> 1; - mergeSort(supp, keys, fromIndex, mid, indices); - mergeSort(supp, keys, mid, toIndex, indices); - - // If list is already sorted, just copy from supp to indices. This is an - // optimization that results in faster sorts for nearly ordered lists. - if (keys[supp[mid]] <= keys[supp[mid - 1]]) { - System.arraycopy(supp, fromIndex, indices, fromIndex, len); - return; - } - - // Merge sorted halves (now in supp) into indices - int i = fromIndex, p = fromIndex, q = mid; - - while (i < toIndex) { - if (q >= toIndex || p < mid && keys[supp[q]] <= keys[supp[p]]) { - indices[i] = supp[p++]; - } else { - indices[i] = supp[q++]; - } - - i++; - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/RadixSort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/RadixSort.java index 00aedcb426..a8b0a0c8d5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/RadixSort.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/RadixSort.java @@ -1,7 +1,9 @@ package net.caffeinemc.mods.sodium.client.util.sorting; -public class RadixSort extends AbstractSort { - public static final int RADIX_SORT_THRESHOLD = 64; +import it.unimi.dsi.fastutil.ints.IntArrays; + +public class RadixSort { + private static final int RADIX_SORT_THRESHOLD = 80; private static final int DIGIT_BITS = 8; private static final int RADIX_KEY_BITS = Integer.BYTES * 8; @@ -9,27 +11,15 @@ public class RadixSort extends AbstractSort { private static final int DIGIT_COUNT = (RADIX_KEY_BITS + DIGIT_BITS - 1) / DIGIT_BITS; private static final int DIGIT_MASK = (1 << DIGIT_BITS) - 1; - public static int[] sort(int[] keys) { - if (keys.length <= 1) { - return new int[keys.length]; - } - - return radixSort(keys, createHistogram(keys)); - } - - private static int[][] createHistogram(int[] keys) { - var histogram = new int[DIGIT_COUNT][BUCKET_COUNT]; - + private static void getHistogram(int[][] histogram, int[] keys) { for (final int key : keys) { for (int digit = 0; digit < DIGIT_COUNT; digit++) { histogram[digit][extractDigit(key, digit)] += 1; } } - - return histogram; } - private static void prefixSum(int[][] offsets) { + private static void prefixSums(int[][] offsets) { for (int digit = 0; digit < DIGIT_COUNT; digit++) { final var buckets = offsets[digit]; var sum = 0; @@ -42,13 +32,65 @@ private static void prefixSum(int[][] offsets) { } } - private static int[] radixSort(int[] keys, int[][] offsets) { - prefixSum(offsets); + /** + *

Sorts the specified array according to the natural ascending order using an out-of-place, indirect, + * 256-way LSD radix sort. This algorithm is well suited for large arrays of integers, especially when they are + * uniformly distributed over the entire range of the data type.

+ * + *

This method implements an indirect sort. The elements of {@param perm} (which must be + * exactly the numbers in the interval {@code [0..perm.length)}) will be permuted so that + * {@code x[perm[i]] < x[perm[i + 1]]}.

+ * + *

While this implementation is very fast on larger arrays, there is a certain amount of fixed cost involved in + * computing the histogram and prefix sums. Because of this, a fallback algorithm (currently quick sort) is used + * for very small arrays to ensure this method performs well for all inputs of all sizes.

+ * + *

If {@param stable} is true, the sorting algorithm is defined to be stable, and duplicate keys will be + * returned in the ordering given by {@param keys}. Using a stable sort is slower than an unstable sort, but the + * overall time complexity and memory requirements are the same.

+ * + * @param perm a permutation array indexing {@param keys}. + * @param keys the array of elements to be sorted. + * @param stable whether the sort is allowed to re-order duplicate keys + */ + public static void sortIndirect(final int[] perm, final int[] keys, boolean stable) { + if (perm.length <= RADIX_SORT_THRESHOLD) { + smallSort(perm, keys, stable); + return; + } + + int[][] offsets; + int[] next; + + try { + offsets = new int[DIGIT_COUNT][BUCKET_COUNT]; + next = new int[perm.length]; + + sortIndirect(perm, keys, offsets, next); + } catch (OutOfMemoryError oom) { + // Not enough memory to perform an out-of-place sort, so use an in-place alternative. + // If the array is very large (hence causing the memory allocation failure), the sort might take a while. + // We need to make sure the allocations are no longer reachable and can be immediately reclaimed. + + // noinspection UnusedAssignment + offsets = null; + // noinspection UnusedAssignment + next = null; - final var length = keys.length; + fallbackSort(perm, keys, stable); + } + } + + private static void sortIndirect(final int[] perm, + final int[] keys, + final int[][] offsets, + int[] next) + { + final int length = perm.length; + getHistogram(offsets, keys); + prefixSums(offsets); - int[] cur = createIndexBuffer(length); - int[] next = new int[length]; + int[] cur = perm; for (int digit = 0; digit < DIGIT_COUNT; digit++) { final var buckets = offsets[digit]; @@ -68,15 +110,26 @@ private static int[] radixSort(int[] keys, int[][] offsets) { cur = temp; } } + } + + private static void smallSort(int[] perm, int[] keys, boolean stable) { + if (perm.length <= 1) { + return; + } - return cur; + fallbackSort(perm, keys, stable); } - private static int extractDigit(int key, int digit) { - return ((key >>> (digit * DIGIT_BITS)) & DIGIT_MASK); + // Fallback sorting method which is guaranteed to be in-place and not require additional memory. + private static void fallbackSort(int[] perm, int[] keys, boolean stable) { + IntArrays.quickSortIndirect(perm, keys); + + if (stable) { + IntArrays.stabilize(perm, keys); + } } - public static boolean useRadixSort(int length) { - return length >= RADIX_SORT_THRESHOLD; + private static int extractDigit(int key, int digit) { + return ((key >>> (digit * DIGIT_BITS)) & DIGIT_MASK); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSorters.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSorters.java index 5a36dd1783..9a40932a1e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSorters.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSorters.java @@ -1,45 +1,204 @@ package net.caffeinemc.mods.sodium.client.util.sorting; import com.mojang.blaze3d.vertex.VertexSorting; +import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.util.MathUtil; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; +import org.joml.Intersectionf; import org.joml.Vector3f; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; public class VertexSorters { - public static VertexSorting sortByDistance(Vector3f origin) { - return new SortByDistance(origin); + public static VertexSortingExtended distance(float x, float y, float z) { + if (x == 0.0f && y == 0.0f && z == 0.0f) { + return SortByDistanceToOrigin.INSTANCE; + } + + return new SortByDistanceToPoint(x, y, z); + } + + public static VertexSortingExtended orthographicZ() { + return SortByOrthographicZ.INSTANCE; + } + + // Slow, should only be used when none of the other classes apply. + public static VertexSortingExtended fallback(VertexSorting.DistanceFunction metric) { + return new SortByFallback(metric); + } + + private abstract static class AbstractSorter implements VertexSortingExtended { + @Override + public final int @NotNull [] sort(Vector3f[] centroids) { + final int length = centroids.length; + final var keys = new int[length]; + final var perm = new int[length]; + + for (int index = 0; index < length; index++) { + keys[index] = ~MathUtil.floatToComparableInt(this.applyMetric(centroids[index])); + perm[index] = index; + } + + RadixSort.sortIndirect(perm, keys, true); + + return perm; + } } - private static class SortByDistance extends AbstractVertexSorter { - private final Vector3f origin; + private static class SortByDistanceToPoint extends AbstractSorter { + private final float x, y, z; - private SortByDistance(Vector3f origin) { - this.origin = origin; + private SortByDistanceToPoint(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; } @Override - protected float getKey(Vector3f position) { - return this.origin.distanceSquared(position); + public float applyMetric(float x, float y, float z) { + float dx = this.x - x; + float dy = this.y - y; + float dz = this.z - z; + + return (dx * dx) + (dy * dy) + (dz * dz); + } + } + + private static class SortByDistanceToOrigin extends AbstractSorter { + private static final SortByDistanceToOrigin INSTANCE = new SortByDistanceToOrigin(); + + @Override + public float applyMetric(float x, float y, float z) { + return (x * x) + (y * y) + (z * z); } } - /** - * Sorts the keys given by the subclass by descending value. - */ - private static abstract class AbstractVertexSorter implements VertexSorting { + private static class SortByOrthographicZ extends AbstractSorter { + private static final SortByOrthographicZ INSTANCE = new SortByOrthographicZ(); + @Override - public final int[] sort(Vector3f[] positions) { - return this.mergeSort(positions); + public float applyMetric(float x, float y, float z) { + return -z; } + } - private int[] mergeSort(Vector3f[] positions) { - final var keys = new float[positions.length]; + private static class SortByFallback extends AbstractSorter { + private final DistanceFunction function; + private final Vector3f scratch = new Vector3f(); + + private SortByFallback(DistanceFunction function) { + this.function = function; + } - for (int index = 0; index < positions.length; index++) { - keys[index] = this.getKey(positions[index]); + @Override + public float applyMetric(float x, float y, float z) { + return this.function.apply(this.scratch.set(x, y, z)); + } + } + + public static int[] sort(ByteBuffer buffer, int vertexCount, int vertexStride, VertexSortingExtended sorting) { + Validate.isTrue(buffer.remaining() >= vertexStride * vertexCount, + "Vertex buffer is not large enough to contain all vertices"); + + if (SodiumClientMod.options().quality.useClosestPointEntitySort) { + if (sorting instanceof VertexSorters.SortByDistanceToPoint pointMetric) { + return sortWithPerspective(buffer, vertexCount, vertexStride, pointMetric, pointMetric.x, pointMetric.y, pointMetric.z); + } else if (sorting instanceof VertexSorters.SortByDistanceToOrigin) { + return sortWithPerspective(buffer, vertexCount, vertexStride, sorting, 0.0f, 0.0f, 0.0f); } + } + + return sortWithCentroid(buffer, vertexCount, vertexStride, sorting); + } + + public static int[] sortWithCentroid(ByteBuffer buffer, int vertexCount, int vertexStride, VertexSortingExtended sorting) { + long pVertex0 = MemoryUtil.memAddress(buffer); + long pVertex2 = MemoryUtil.memAddress(buffer, vertexStride * 2); + + int primitiveCount = vertexCount / 4; + int primitiveStride = vertexStride * 4; - return MergeSort.mergeSort(keys); + final int[] keys = new int[primitiveCount]; + final int[] perm = new int[primitiveCount]; + + for (int primitiveId = 0; primitiveId < primitiveCount; primitiveId++) { + // Position of vertex[0] + float v0x = MemoryUtil.memGetFloat(pVertex0 + 0L); + float v0y = MemoryUtil.memGetFloat(pVertex0 + 4L); + float v0z = MemoryUtil.memGetFloat(pVertex0 + 8L); + + // Position of vertex[2] + float v2x = MemoryUtil.memGetFloat(pVertex2 + 0L); + float v2y = MemoryUtil.memGetFloat(pVertex2 + 4L); + float v2z = MemoryUtil.memGetFloat(pVertex2 + 8L); + + // The centroid of the quad is calculated using the mid-point of the diagonal edge. This will not work + // for degenerate quads, but those are not sortable anyway. + float cx = (v0x + v2x) * 0.5F; + float cy = (v0y + v2y) * 0.5F; + float cz = (v0z + v2z) * 0.5F; + + // The sign bit of the metric is negated as we need back-to-front (descending) ordering. + keys[primitiveId] = MathUtil.floatToComparableInt(-sorting.applyMetric(cx, cy, cz)); + perm[primitiveId] = primitiveId; + + pVertex0 += primitiveStride; + pVertex2 += primitiveStride; } - protected abstract float getKey(Vector3f object); + RadixSort.sortIndirect(perm, keys, true); + + return perm; + } + + public static int[] sortWithPerspective(ByteBuffer buffer, int vertexCount, int vertexStride, VertexSortingExtended metric, float refX, float refY, float refZ) { + long pVertex0 = MemoryUtil.memAddress(buffer); + long pVertex1 = MemoryUtil.memAddress(buffer, vertexStride); + long pVertex2 = MemoryUtil.memAddress(buffer, vertexStride * 2); + + int primitiveCount = vertexCount / 4; + int primitiveStride = vertexStride * 4; + + final int[] keys = new int[primitiveCount]; + final int[] perm = new int[primitiveCount]; + + final var scratch = new Vector3f(); + + for (int primitiveId = 0; primitiveId < primitiveCount; primitiveId++) { + // instead of calculating the centroid, calculate the closest point on the quad (assuming it's flat and rectangular) to the reference, which may not be the centroid + float v0x = MemoryUtil.memGetFloat(pVertex0 + 0L); + float v0y = MemoryUtil.memGetFloat(pVertex0 + 4L); + float v0z = MemoryUtil.memGetFloat(pVertex0 + 8L); + + float v1x = MemoryUtil.memGetFloat(pVertex1 + 0L); + float v1y = MemoryUtil.memGetFloat(pVertex1 + 4L); + float v1z = MemoryUtil.memGetFloat(pVertex1 + 8L); + + float v2x = MemoryUtil.memGetFloat(pVertex2 + 0L); + float v2y = MemoryUtil.memGetFloat(pVertex2 + 4L); + float v2z = MemoryUtil.memGetFloat(pVertex2 + 8L); + + // this method requires the first point to be between the second and third in the rectangle + Intersectionf.findClosestPointOnRectangle( + v1x, v1y, v1z, + v0x, v0y, v0z, + v2x, v2y, v2z, + refX, refY, refZ, + scratch); + + // The sign bit of the metric is negated as we need back-to-front (descending) ordering. + keys[primitiveId] = MathUtil.floatToComparableInt(-metric.applyMetric(scratch.x, scratch.y, scratch.z)); + perm[primitiveId] = primitiveId; + + pVertex0 += primitiveStride; + pVertex1 += primitiveStride; + pVertex2 += primitiveStride; + } + + RadixSort.sortIndirect(perm, keys, true); + + return perm; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSortingExtended.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSortingExtended.java new file mode 100644 index 0000000000..b04bc9eb99 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/sorting/VertexSortingExtended.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.client.util.sorting; + +import com.mojang.blaze3d.vertex.VertexSorting; +import org.joml.Vector3f; + +public interface VertexSortingExtended extends VertexSorting { + float applyMetric(float x, float y, float z); + + default float applyMetric(Vector3f vector) { + return this.applyMetric(vector.x, vector.y, vector.z); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/LevelSlice.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/LevelSlice.java index c6b18eac52..969258eefc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/LevelSlice.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/LevelSlice.java @@ -1,9 +1,11 @@ package net.caffeinemc.mods.sodium.client.world; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; -import net.caffeinemc.mods.sodium.client.services.*; -import net.caffeinemc.mods.sodium.client.world.biome.LevelColorCache; +import net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks; +import net.caffeinemc.mods.sodium.client.services.SodiumModelData; +import net.caffeinemc.mods.sodium.client.services.SodiumModelDataContainer; import net.caffeinemc.mods.sodium.client.world.biome.LevelBiomeSlice; +import net.caffeinemc.mods.sodium.client.world.biome.LevelColorCache; import net.caffeinemc.mods.sodium.client.world.cloned.ChunkRenderContext; import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSection; import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSectionCache; @@ -353,6 +355,10 @@ public int getBlockTint(BlockPos pos, ColorResolver resolver) { return this.biomeColors.getColor(resolver, pos.getX(), pos.getY(), pos.getZ()); } + public boolean hasBiomeBlend() { + return this.biomeColors.getBlendRadius() > 0; + } + @Override public int getHeight() { return this.level.getHeight(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/biome/LevelColorCache.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/biome/LevelColorCache.java index 80561d0fc3..582f62dd85 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/biome/LevelColorCache.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/biome/LevelColorCache.java @@ -4,7 +4,6 @@ import net.caffeinemc.mods.sodium.client.util.color.BoxBlur; import net.caffeinemc.mods.sodium.client.util.color.BoxBlur.ColorBuffer; import net.caffeinemc.mods.sodium.client.world.cloned.ChunkRenderContext; -import net.minecraft.client.renderer.BiomeColors; import net.minecraft.util.Mth; import net.minecraft.world.level.ColorResolver; import net.minecraft.world.level.biome.Biome; @@ -39,35 +38,36 @@ public LevelColorCache(LevelBiomeSlice biomeData, int blendRadius) { } public void update(ChunkRenderContext context) { - this.minBlockX = (context.getOrigin().minBlockX() - NEIGHBOR_BLOCK_RADIUS) - this.blendRadius; + this.minBlockX = (context.getOrigin().minBlockX() - NEIGHBOR_BLOCK_RADIUS); this.minBlockY = (context.getOrigin().minBlockY() - NEIGHBOR_BLOCK_RADIUS); - this.minBlockZ = (context.getOrigin().minBlockZ() - NEIGHBOR_BLOCK_RADIUS) - this.blendRadius; + this.minBlockZ = (context.getOrigin().minBlockZ() - NEIGHBOR_BLOCK_RADIUS); - this.maxBlockX = (context.getOrigin().maxBlockX() + NEIGHBOR_BLOCK_RADIUS) + this.blendRadius; + this.maxBlockX = (context.getOrigin().maxBlockX() + NEIGHBOR_BLOCK_RADIUS); this.maxBlockY = (context.getOrigin().maxBlockY() + NEIGHBOR_BLOCK_RADIUS); - this.maxBlockZ = (context.getOrigin().maxBlockZ() + NEIGHBOR_BLOCK_RADIUS) + this.blendRadius; + this.maxBlockZ = (context.getOrigin().maxBlockZ() + NEIGHBOR_BLOCK_RADIUS); this.populateStamp++; } public int getColor(ColorResolver resolver, int blockX, int blockY, int blockZ) { - var relBlockX = Mth.clamp(blockX, this.minBlockX, this.maxBlockX) - this.minBlockX; - var relBlockY = Mth.clamp(blockY, this.minBlockY, this.maxBlockY) - this.minBlockY; - var relBlockZ = Mth.clamp(blockZ, this.minBlockZ, this.maxBlockZ) - this.minBlockZ; + // Clamp inputs + blockX = Mth.clamp(blockX, this.minBlockX, this.maxBlockX) - this.minBlockX; + blockY = Mth.clamp(blockY, this.minBlockY, this.maxBlockY) - this.minBlockY; + blockZ = Mth.clamp(blockZ, this.minBlockZ, this.maxBlockZ) - this.minBlockZ; if (!this.slices.containsKey(resolver)) { this.initializeSlices(resolver); } - var slice = this.slices.get(resolver)[relBlockY]; + var slice = this.slices.get(resolver)[blockY]; if (slice.lastPopulateStamp < this.populateStamp) { - this.updateColorBuffers(relBlockY, resolver, slice); + this.updateColorBuffers(blockY, resolver, slice); } var buffer = slice.getBuffer(); - return buffer.get(relBlockX, relBlockZ); + return buffer.get(blockX + this.blendRadius, blockZ + this.blendRadius); } private void initializeSlices(ColorResolver resolver) { @@ -83,24 +83,36 @@ private void initializeSlices(ColorResolver resolver) { private void updateColorBuffers(int relY, ColorResolver resolver, Slice slice) { int blockY = this.minBlockY + relY; - for (int blockZ = this.minBlockZ; blockZ <= this.maxBlockZ; blockZ++) { - for (int blockX = this.minBlockX; blockX <= this.maxBlockX; blockX++) { + int minBlockZ = this.minBlockZ - this.blendRadius; + int minBlockX = this.minBlockX - this.blendRadius; + + int maxBlockZ = this.maxBlockZ + this.blendRadius; + int maxBlockX = this.maxBlockX + this.blendRadius; + + ColorBuffer buffer = slice.buffer; + + for (int blockZ = minBlockZ; blockZ <= maxBlockZ; blockZ++) { + for (int blockX = minBlockX; blockX <= maxBlockX; blockX++) { Biome biome = this.biomeData.getBiome(blockX, blockY, blockZ).value(); - int relBlockX = blockX - this.minBlockX; - int relBlockZ = blockZ - this.minBlockZ; + int relBlockX = blockX - minBlockX; + int relBlockZ = blockZ - minBlockZ; - slice.buffer.set(relBlockX, relBlockZ, resolver.getColor(biome, blockX, blockZ)); + buffer.set(relBlockX, relBlockZ, resolver.getColor(biome, blockX, blockZ)); } } if (this.blendRadius > 0) { - BoxBlur.blur(slice.buffer, this.tempColorBuffer, this.blendRadius); + BoxBlur.blur(buffer.data, this.tempColorBuffer.data, this.sizeXZ, this.sizeXZ, this.blendRadius); } slice.lastPopulateStamp = this.populateStamp; } + public int getBlendRadius() { + return this.blendRadius; + } + private static class Slice { private final ColorBuffer buffer; private long lastPopulateStamp; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ChunkRenderContext.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ChunkRenderContext.java index 0242748dcc..9f31b4b998 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ChunkRenderContext.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ChunkRenderContext.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.client.world.cloned; -import net.caffeinemc.mods.sodium.client.services.SodiumModelDataContainer; import net.minecraft.core.SectionPos; import net.minecraft.world.level.levelgen.structure.BoundingBox; @@ -32,6 +31,6 @@ public BoundingBox getVolume() { } public List getRenderers() { - return renderers; + return this.renderers; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ClonedChunkSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ClonedChunkSection.java index 6390b26921..2c0035407e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ClonedChunkSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/world/cloned/ClonedChunkSection.java @@ -4,10 +4,10 @@ import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import net.caffeinemc.mods.sodium.client.services.*; -import net.caffeinemc.mods.sodium.client.world.PalettedContainerROExtension; +import net.caffeinemc.mods.sodium.client.util.iterator.WrappedIterator; import net.caffeinemc.mods.sodium.client.world.LevelSlice; +import net.caffeinemc.mods.sodium.client.world.PalettedContainerROExtension; import net.caffeinemc.mods.sodium.client.world.SodiumAuxiliaryLightManager; -import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.SectionPos; import net.minecraft.world.level.Level; @@ -17,18 +17,12 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.DataLayer; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.chunk.LevelChunkSection; -import net.minecraft.world.level.chunk.PalettedContainer; -import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.chunk.*; import net.minecraft.world.level.levelgen.DebugLevelSource; import net.minecraft.world.level.levelgen.structure.BoundingBox; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Map; - public class ClonedChunkSection { private static final DataLayer DEFAULT_SKY_LIGHT_ARRAY = new DataLayer(15); private static final DataLayer DEFAULT_BLOCK_LIGHT_ARRAY = new DataLayer(0); @@ -58,7 +52,7 @@ public ClonedChunkSection(Level level, LevelChunk chunk, @Nullable LevelChunkSec Int2ReferenceMap blockEntityMap = null; Int2ReferenceMap blockEntityRenderDataMap = null; SodiumModelDataContainer modelMap = PlatformModelAccess.getInstance().getModelDataContainer(level, pos); - auxLightManager = PlatformLevelAccess.INSTANCE.getLightManager(chunk, pos); + this.auxLightManager = PlatformLevelAccess.INSTANCE.getLightManager(chunk, pos); if (section != null) { if (!section.hasOnlyAir()) { @@ -67,7 +61,7 @@ public ClonedChunkSection(Level level, LevelChunk chunk, @Nullable LevelChunkSec } else { blockData = constructDebugWorldContainer(pos); } - blockEntityMap = copyBlockEntities(chunk, pos); + blockEntityMap = tryCopyBlockEntities(chunk, pos); if (blockEntityMap != null && PlatformBlockAccess.getInstance().platformHasBlockData()) { blockEntityRenderDataMap = copyBlockEntityRenderData(level, blockEntityMap); } @@ -150,6 +144,30 @@ private static DataLayer copyLightArray(Level level, LightLayer type, SectionPos return array; } + @Nullable + private static Int2ReferenceMap tryCopyBlockEntities(LevelChunk chunk, SectionPos chunkCoord) { + try { + // Some mods are violating memory safety, and the block entity iterator occasionally returns garbage results + // or otherwise throws exceptions because of this. To better diagnose these crashes, wrap the iterator + // so that we can handle any uncaught exceptions with the following special case. + return copyBlockEntities(chunk, chunkCoord); + } catch (WrappedIterator.Exception t) { + // Very infrequent check, only going to be called on game crash. Don't bother caching this. + if (PlatformRuntimeInformation.getInstance().isModInLoadingList("entityculling")) { + // The Entity Culling mod is known to mangle the block entity set, so try to attribute it directly + // if we know it's installed. Yes, this is accusatory, but we are tired of these cryptic crashes, + // and users need more information about how to resolve the problem themselves. This was the + // second-best option to outright preventing the launch of Sodium when Entity Culling is installed. + throw new RuntimeException("Failed to iterate block entities! This is *very likely* the fault of the " + + "Entity Culling mod, and cannot be fixed by Sodium. See here for more details: " + + "https://link.caffeinemc.net/help/sodium/mod-issue/entity-culling/gh-2985", t); + } else { + throw new RuntimeException("Failed to iterate block entities! This is *very likely* the fault of " + + "another misbehaving mod, not Sodium. Please check your mods list.", t); + } + } + } + @Nullable private static Int2ReferenceMap copyBlockEntities(LevelChunk chunk, SectionPos chunkCoord) { BoundingBox box = new BoundingBox(chunkCoord.minBlockX(), chunkCoord.minBlockY(), chunkCoord.minBlockZ(), @@ -157,10 +175,14 @@ private static Int2ReferenceMap copyBlockEntities(LevelChunk chunk, Int2ReferenceOpenHashMap blockEntities = null; + // Catch exceptions thrown by the iterator and handle them specially via the wrapped exception type + var it = WrappedIterator.create(chunk.getBlockEntities().entrySet()); + // Copy the block entities from the chunk into our cloned section - for (Map.Entry entry : chunk.getBlockEntities().entrySet()) { - BlockPos pos = entry.getKey(); - BlockEntity entity = entry.getValue(); + while (it.hasNext()) { + var entry = it.next(); + var pos = entry.getKey(); + var entity = entry.getValue(); if (box.isInside(pos)) { if (blockEntities == null) { @@ -226,7 +248,7 @@ public SectionPos getPosition() { } public SodiumModelDataContainer getModelMap() { - return modelMap; + return this.modelMap; } public @Nullable DataLayer getLightArray(LightLayer lightType) { @@ -242,6 +264,6 @@ public void setLastUsedTimestamp(long timestamp) { } public SodiumAuxiliaryLightManager getAuxLightManager() { - return auxLightManager; + return this.auxLightManager; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/SodiumMixinPlugin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/SodiumMixinPlugin.java index 091a950c4d..e5101ceb74 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/SodiumMixinPlugin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/SodiumMixinPlugin.java @@ -30,7 +30,7 @@ public void onLoad(String mixinPackage) { this.dependencyResolutionFailed = PlatformRuntimeInformation.getInstance().isModInLoadingList("embeddium"); - if (dependencyResolutionFailed) { + if (this.dependencyResolutionFailed) { this.logger.error("Not applying any Sodium mixins; dependency resolution has failed."); } @@ -45,7 +45,7 @@ public String getRefMapperConfig() { @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - if (dependencyResolutionFailed) { + if (this.dependencyResolutionFailed) { return false; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/MinecraftMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/MinecraftMixin.java index df0e469db2..632dc0901e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/MinecraftMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/MinecraftMixin.java @@ -3,10 +3,13 @@ import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.checks.ResourcePackScanner; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.client.util.FrameTimeStatistics; import net.minecraft.client.Minecraft; import net.minecraft.server.packs.resources.ReloadableResourceManager; import net.minecraft.util.profiling.ProfilerFiller; import org.lwjgl.opengl.GL32C; +import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -15,6 +18,7 @@ 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.concurrent.CompletableFuture; @Mixin(Minecraft.class) @@ -77,6 +81,8 @@ private void postRender(boolean tick, CallbackInfo ci) { @Inject(method = "buildInitialScreens", at = @At("TAIL")) private void postInit(CallbackInfoReturnable cir) { ResourcePackScanner.checkIfCoreShaderLoaded(this.resourceManager); + + ConfigManager.registerConfigsLate(); } /** @@ -87,4 +93,14 @@ private void postResourceReload(CallbackInfoReturnable> ResourcePackScanner.checkIfCoreShaderLoaded(this.resourceManager); } + /** + * hook the vanilla fps update to update our fps display only when it does too, once a second + */ + @Inject( + method = "runTick", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;fps:I", opcode = Opcodes.PUTSTATIC, shift = At.Shift.AFTER) + ) + private void sodium$updatePercentileCache(boolean advanceGameTime, CallbackInfo ci) { + FrameTimeStatistics.INSTANCE.invalidate(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/WindowMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/WindowMixin.java index fc8592bddf..a87357da30 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/WindowMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/WindowMixin.java @@ -3,9 +3,9 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.blaze3d.platform.Window; -import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWNativeWin32; @@ -20,11 +20,14 @@ public class WindowMixin implements NativeWindowHandle { @Final private long window; - @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwCreateWindow(IILjava/lang/CharSequence;JJ)J"), require = 0) + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwCreateWindow(IILjava/lang/CharSequence;JJ)J"), require = 0, expect = 0) public long setAdditionalWindowHints(int titleEncoded, int width, CharSequence height, long title, long monitor, Operation original) { - if (!PlatformRuntimeInformation.getInstance().platformHasEarlyLoadingScreen() && SodiumClientMod.options().performance.useNoErrorGLContext && - !Workarounds.isWorkaroundEnabled(Workarounds.Reference.NO_ERROR_CONTEXT_UNSUPPORTED)) { - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_NO_ERROR, GLFW.GLFW_TRUE); + if (!PlatformRuntimeInformation.getInstance().platformHasEarlyLoadingScreen()) { + if (SodiumClientMod.options().performance.useNoErrorGLContext) { + if (!Workarounds.isWorkaroundEnabled(Workarounds.Reference.NO_ERROR_CONTEXT_UNSUPPORTED)) { + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_NO_ERROR, GLFW.GLFW_TRUE); + } + } } return original.call(titleEncoded, width, height, title, monitor); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/colors/BlockColorsMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/colors/BlockColorsMixin.java index ca9a2f192e..92a74608f7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/colors/BlockColorsMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/colors/BlockColorsMixin.java @@ -26,7 +26,7 @@ public class BlockColorsMixin implements BlockColorsExtension { private void preRegisterColorProvider(BlockColor provider, Block[] blocks, CallbackInfo ci) { for (Block block : blocks) { // There will be one provider already registered for vanilla blocks, if we are replacing it, - // it means a mod is using custom logic and we need to disable per-vertex coloring + // it means a mod is using custom logic, and we need to disable per-vertex coloring if (this.blocksToColor.put(block, provider) != null) { this.overridenBlocks.add(block); SodiumClientMod.logger().info("Block {} had its color provider replaced with {} and will not use per-vertex coloring", BuiltInRegistries.BLOCK.getKey(block), provider.toString()); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/BlockEntityTypeMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/BlockEntityTypeMixin.java index 63709d4ce4..ce89c509c2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/BlockEntityTypeMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/BlockEntityTypeMixin.java @@ -1,7 +1,5 @@ package net.caffeinemc.mods.sodium.mixin.core.render; -import java.util.function.Predicate; - import net.caffeinemc.mods.sodium.api.blockentity.BlockEntityRenderPredicate; import net.caffeinemc.mods.sodium.client.render.chunk.ExtendedBlockEntityType; import net.minecraft.world.level.block.entity.BlockEntity; @@ -17,23 +15,23 @@ public class BlockEntityTypeMixin implements ExtendedBloc @Override public BlockEntityRenderPredicate[] sodium$getRenderPredicates() { - return sodium$renderPredicates; + return this.sodium$renderPredicates; } @Override public void sodium$addRenderPredicate(BlockEntityRenderPredicate predicate) { - sodium$renderPredicates = ArrayUtils.add(sodium$renderPredicates, predicate); + this.sodium$renderPredicates = ArrayUtils.add(this.sodium$renderPredicates, predicate); } @Override public boolean sodium$removeRenderPredicate(BlockEntityRenderPredicate predicate) { - int index = ArrayUtils.indexOf(sodium$renderPredicates, predicate); + int index = ArrayUtils.indexOf(this.sodium$renderPredicates, predicate); if (index == ArrayUtils.INDEX_NOT_FOUND) { return false; } - sodium$renderPredicates = ArrayUtils.remove(sodium$renderPredicates, index); + this.sodium$renderPredicates = ArrayUtils.remove(this.sodium$renderPredicates, index); return true; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/BufferBuilderMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/BufferBuilderMixin.java index ec198c1731..c1e87ecf6c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/BufferBuilderMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/BufferBuilderMixin.java @@ -48,6 +48,11 @@ public abstract class BufferBuilderMixin implements VertexBufferWriter, BufferBu this.vertices++; } + @Override + public VertexFormat sodium$getVertexFormat() { + return this.format; + } + @Override public void push(MemoryStack stack, long src, int count, VertexFormat format) { var length = count * this.vertexSize; @@ -66,7 +71,7 @@ public void push(MemoryStack stack, long src, int count, VertexFormat format) { } this.vertices += count; - this.vertexPointer = (dst + length) - vertexSize; + this.vertexPointer = (dst + length) - this.vertexSize; this.elementsToFill = 0; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/EntityOutlineGeneratorMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/EntityOutlineGeneratorMixin.java index 015277efce..a4664e270c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/EntityOutlineGeneratorMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/EntityOutlineGeneratorMixin.java @@ -37,6 +37,11 @@ public boolean canUseIntrinsics() { return this.canUseIntrinsics; } + @Override + public boolean canUseIntrinsics(VertexFormat format) { + return this.canUseIntrinsics && VertexBufferWriter.tryOf(this.delegate, format) != null; + } + @Override public void push(MemoryStack stack, long ptr, int count, VertexFormat format) { transform(ptr, count, format, diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SheetedDecalTextureGeneratorMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SheetedDecalTextureGeneratorMixin.java index b08314a264..84a47942bb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SheetedDecalTextureGeneratorMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SheetedDecalTextureGeneratorMixin.java @@ -55,6 +55,11 @@ public boolean canUseIntrinsics() { return this.canUseIntrinsics; } + @Override + public boolean canUseIntrinsics(VertexFormat format) { + return this.canUseIntrinsics && VertexBufferWriter.tryOf(this.delegate, format) != null; + } + @Override public void push(MemoryStack stack, long ptr, int count, VertexFormat format) { transform(ptr, count, format, diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SpriteCoordinateExpanderMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SpriteCoordinateExpanderMixin.java index a5791813ed..66a0848355 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SpriteCoordinateExpanderMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/SpriteCoordinateExpanderMixin.java @@ -47,6 +47,11 @@ public boolean canUseIntrinsics() { return this.canUseIntrinsics; } + @Override + public boolean canUseIntrinsics(VertexFormat format) { + return this.canUseIntrinsics && VertexBufferWriter.tryOf(this.delegate, format) != null; + } + @Override public void push(MemoryStack stack, final long ptr, int count, VertexFormat format) { transform(ptr, count, format, diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/VertexMultiConsumerMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/VertexMultiConsumerMixin.java index 551492f3ea..3849caf352 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/VertexMultiConsumerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/immediate/consumer/VertexMultiConsumerMixin.java @@ -1,8 +1,8 @@ package net.caffeinemc.mods.sodium.mixin.core.render.immediate.consumer; -import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; import org.lwjgl.system.MemoryStack; import org.spongepowered.asm.mixin.Final; @@ -37,6 +37,13 @@ public boolean canUseIntrinsics() { return this.canUseIntrinsics; } + @Override + public boolean canUseIntrinsics(VertexFormat format) { + return this.canUseIntrinsics && + VertexBufferWriter.tryOf(this.first, format) != null && + VertexBufferWriter.tryOf(this.second, format) != null; + } + @Override public void push(MemoryStack stack, long ptr, int count, VertexFormat format) { VertexBufferWriter.copyInto(VertexBufferWriter.of(this.first), stack, ptr, count, format); @@ -55,7 +62,7 @@ public static class MultipleMixin implements VertexBufferWriter { @Inject(method = "", at = @At("RETURN")) private void checkFullStatus(CallbackInfo ci) { - this.canUseIntrinsics = allDelegatesSupportIntrinsics(); + this.canUseIntrinsics = this.allDelegatesSupportIntrinsics(); } @Unique @@ -74,6 +81,21 @@ public boolean canUseIntrinsics() { return this.canUseIntrinsics; } + @Override + public boolean canUseIntrinsics(VertexFormat format) { + if (!this.canUseIntrinsics) { + return false; + } + + for (var delegate : this.delegates) { + if (VertexBufferWriter.tryOf(delegate, format) == null) { + return false; + } + } + + return true; + } + @Override public void push(MemoryStack stack, long ptr, int count, VertexFormat format) { for (var delegate : this.delegates) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/texture/TextureAtlasAccessor.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/texture/TextureAtlasAccessor.java index f95d1f8f59..8d12e742bf 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/texture/TextureAtlasAccessor.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/texture/TextureAtlasAccessor.java @@ -1,15 +1,14 @@ package net.caffeinemc.mods.sodium.mixin.core.render.texture; import net.minecraft.client.renderer.texture.TextureAtlas; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -import java.util.Map; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(TextureAtlas.class) public interface TextureAtlasAccessor { - @Accessor - Map getTexturesByName(); + @Invoker("getWidth") + int sodium$getWidth(); + + @Invoker("getHeight") + int sodium$getHeight(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/world/LevelRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/world/LevelRendererMixin.java index 75f229c2be..82b994bcbe 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/world/LevelRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/core/render/world/LevelRendererMixin.java @@ -214,7 +214,7 @@ private void onRenderBlockEntities(DeltaTracker deltaTracker, boolean bl, Camera // Exclusive to NeoForge, allow to fail. @SuppressWarnings("all") - @Inject(method = "iterateVisibleBlockEntities", at = @At("HEAD"), cancellable = true, require = 0) + @Inject(method = "iterateVisibleBlockEntities", at = @At("HEAD"), cancellable = true, expect = 0, require = 0) public void replaceBlockEntityIteration(Consumer blockEntityConsumer, CallbackInfo ci) { ci.cancel(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/debug/checks/threading/MixinRenderSystem.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/debug/checks/threading/MixinRenderSystem.java new file mode 100644 index 0000000000..e0b027bd0e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/debug/checks/threading/MixinRenderSystem.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.mixin.debug.checks.threading; + +import com.mojang.blaze3d.pipeline.RenderCall; +import com.mojang.blaze3d.systems.RenderSystem; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(RenderSystem.class) +public abstract class MixinRenderSystem { + /** + * @author JellySquid + * @reason Disallow the use of RenderSystem.recordRenderCall entirely + */ + @Overwrite(remap = false) + public static void recordRenderCall(RenderCall call) { + throw new UnsupportedOperationException("Usage of RenderSystem#recordRenderCall is likely a bug, " + + "which is handled as an error when Sodium is enabled in debug mode"); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/debug/DebugScreenOverlayMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/debug/DebugScreenOverlayMixin.java index eedb7302f9..1d32083b34 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/debug/DebugScreenOverlayMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/debug/DebugScreenOverlayMixin.java @@ -1,8 +1,10 @@ package net.caffeinemc.mods.sodium.mixin.features.gui.hooks.debug; import com.google.common.collect.Lists; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer; +import net.caffeinemc.mods.sodium.client.util.FrameTimeStatistics; import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.NativeBuffer; import net.minecraft.ChatFormatting; @@ -10,10 +12,13 @@ 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; import java.lang.management.ManagementFactory; import java.util.ArrayList; +import java.util.List; @Mixin(DebugScreenOverlay.class) public abstract class DebugScreenOverlayMixin { @@ -67,4 +72,44 @@ private static String getNativeMemoryString() { private static long getNativeMemoryUsage() { return ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getUsed() + NativeBuffer.getTotalAllocated(); } + + /** + * Maintain our own frame-time ring buffer (with more samples than vanilla's FPS + * graph storage) by capturing every frame duration that vanilla logs. + */ + @Inject(method = "logFrameDuration", at = @At("HEAD")) + private void sodium$captureFrameDuration(long frameDuration, CallbackInfo ci) { + FrameTimeStatistics.INSTANCE.logSample(frameDuration); + } + + @ModifyReturnValue(method = "getGameInformation", at = @At("RETURN")) + private List sodium$insertFpsPercentiles(List lines) { + var results = FrameTimeStatistics.INSTANCE.get(); + if (results == null || results.isEmpty()) { + return lines; + } + + var sb = new StringBuilder(); + for (var entry : results.reference2LongEntrySet()) { + if (!sb.isEmpty()) { + sb.append(' '); + } + long ns = entry.getLongValue(); + sb.append(ChatFormatting.GRAY) + .append(entry.getKey().name()).append('=') + .append(ChatFormatting.RESET) + .append(sodium$nanosToFps(ns)); + } + sb.append(ChatFormatting.GRAY).append(" fps"); + + // put it right after the fps line, which vanilla always emits as the second entry + lines.add(2, sb.toString()); + + return lines; + } + + @Unique + private static long sodium$nanosToFps(long ns) { + return ns > 0L ? Math.round(1.0e9 / ns) : 0L; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/settings/OptionsScreenMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/settings/OptionsScreenMixin.java index 0e1c1d27ec..5b5d303952 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/settings/OptionsScreenMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/hooks/settings/OptionsScreenMixin.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.mixin.features.gui.hooks.settings; -import net.caffeinemc.mods.sodium.client.gui.SodiumOptionsGUI; +import net.caffeinemc.mods.sodium.client.gui.VideoSettingsScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.options.OptionsScreen; import net.minecraft.network.chat.Component; @@ -22,6 +22,6 @@ protected OptionsScreenMixin(Component title) { "lambda$init$2" }, require = 1, at = @At("HEAD"), cancellable = true) private void open(CallbackInfoReturnable ci) { - ci.setReturnValue(SodiumOptionsGUI.createScreen(this)); + ci.setReturnValue(VideoSettingsScreen.createScreen(this)); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/screen/LevelLoadingScreenMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/screen/LevelLoadingScreenMixin.java index 6f649536a0..0e09b202d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/screen/LevelLoadingScreenMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/gui/screen/LevelLoadingScreenMixin.java @@ -1,16 +1,14 @@ package net.caffeinemc.mods.sodium.mixin.features.gui.screen; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.*; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -import net.caffeinemc.mods.sodium.api.vertex.format.common.ColorVertex; -import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.util.ColorARGB; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.ColorVertex; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.LevelLoadingScreen; -import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderType; import net.minecraft.server.level.progress.StoringChunkProgressListener; import net.minecraft.world.level.chunk.status.ChunkStatus; import org.joml.Matrix4f; @@ -47,7 +45,26 @@ public class LevelLoadingScreenMixin { * @author JellySquid */ @Overwrite - public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListener tracker, int mapX, int mapY, int mapScale, int mapPadding) { + public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListener listener, int mapX, int mapY, int mapScale, int mapPadding) { + Matrix4f pose = graphics.pose() + .last() + .pose(); + + // this is just missing the drawSpecial that doesn't exist on 1.21.1 but it works with bufferSource() + var writer = VertexBufferWriter.of(graphics.bufferSource().getBuffer(RenderType.gui())); + + sodium$drawChunkMap(listener, mapX, mapY, mapScale, mapPadding, writer, pose); + } + + @Unique + private static void sodium$drawChunkMap(StoringChunkProgressListener listener, + int mapX, + int mapY, + int mapScale, + int mapPadding, + VertexBufferWriter writer, + Matrix4f pose) + { if (STATUS_TO_COLOR_FAST == null) { STATUS_TO_COLOR_FAST = new Reference2IntOpenHashMap<>(COLORS.size()); STATUS_TO_COLOR_FAST.put(null, NULL_STATUS_COLOR); @@ -55,21 +72,8 @@ public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListen .forEach(entry -> STATUS_TO_COLOR_FAST.put(entry.getKey(), ColorARGB.toABGR(entry.getIntValue(), 0xFF))); } - RenderSystem.setShader(GameRenderer::getPositionColorShader); - - Matrix4f matrix = graphics.pose().last().pose(); - - Tesselator tessellator = Tesselator.getInstance(); - - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - - BufferBuilder bufferBuilder = tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); - - var writer = VertexBufferWriter.of(bufferBuilder); - - int centerSize = tracker.getFullDiameter(); - int size = tracker.getDiameter(); + int centerSize = listener.getFullDiameter(); + int size = listener.getDiameter(); int tileSize = mapScale + mapPadding; @@ -77,10 +81,10 @@ public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListen int mapRenderCenterSize = centerSize * tileSize - mapPadding; int radius = mapRenderCenterSize / 2 + 1; - addRect(writer, matrix, mapX - radius, mapY - radius, mapX - radius + 1, mapY + radius, DEFAULT_STATUS_COLOR); - addRect(writer, matrix, mapX + radius - 1, mapY - radius, mapX + radius, mapY + radius, DEFAULT_STATUS_COLOR); - addRect(writer, matrix, mapX - radius, mapY - radius, mapX + radius, mapY - radius + 1, DEFAULT_STATUS_COLOR); - addRect(writer, matrix, mapX - radius, mapY + radius - 1, mapX + radius, mapY + radius, DEFAULT_STATUS_COLOR); + addRect(writer, pose, mapX - radius, mapY - radius, mapX - radius + 1, mapY + radius, DEFAULT_STATUS_COLOR); + addRect(writer, pose, mapX + radius - 1, mapY - radius, mapX + radius, mapY + radius, DEFAULT_STATUS_COLOR); + addRect(writer, pose, mapX - radius, mapY - radius, mapX + radius, mapY - radius + 1, DEFAULT_STATUS_COLOR); + addRect(writer, pose, mapX - radius, mapY + radius - 1, mapX + radius, mapY + radius, DEFAULT_STATUS_COLOR); } int mapRenderSize = size * tileSize - mapPadding; @@ -96,7 +100,7 @@ public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListen for (int z = 0; z < size; ++z) { int tileY = mapStartY + z * tileSize; - ChunkStatus status = tracker.getStatus(x, z); + ChunkStatus status = listener.getStatus(x, z); int color; if (prevStatus == status) { @@ -108,18 +112,9 @@ public static void renderChunks(GuiGraphics graphics, StoringChunkProgressListen prevColor = color; } - addRect(writer, matrix, tileX, tileY, tileX + mapScale, tileY + mapScale, color); + addRect(writer, pose, tileX, tileY, tileX + mapScale, tileY + mapScale, color); } } - - MeshData data = bufferBuilder.build(); - - if (data != null) { - BufferUploader.drawWithShader(data); - } - Tesselator.getInstance().clear(); - - RenderSystem.disableBlend(); } @Unique diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/CubeMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/CubeMixin.java index d1590c9c14..6a793362e7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/CubeMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/CubeMixin.java @@ -4,6 +4,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer; import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; import net.caffeinemc.mods.sodium.client.render.immediate.model.EntityRenderer; import net.caffeinemc.mods.sodium.client.render.immediate.model.ModelCuboid; import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils; @@ -38,7 +39,7 @@ private void onInit(ModelPart.Cube instance, float value, int u, int v, float x, @Inject(method = "compile", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack$Pose;pose()Lorg/joml/Matrix4f;"), cancellable = true) private void onCompile(PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color, CallbackInfo ci) { - VertexBufferWriter writer = VertexConsumerUtils.convertOrLog(buffer); + VertexBufferWriter writer = VertexConsumerUtils.convertOrLog(buffer, EntityVertex.FORMAT); if (writer == null) { return; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/ModelPartMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/ModelPartMixin.java index e5a20fa916..08c31f6f13 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/ModelPartMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/ModelPartMixin.java @@ -1,14 +1,11 @@ package net.caffeinemc.mods.sodium.mixin.features.render.entity; -import net.caffeinemc.mods.sodium.client.render.immediate.model.EntityRenderer; +import com.mojang.blaze3d.vertex.PoseStack; import net.caffeinemc.mods.sodium.api.math.MatrixHelper; import net.minecraft.client.model.geom.ModelPart; -import org.spongepowered.asm.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.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; @Mixin(ModelPart.class) public class ModelPartMixin { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/shadows/EntityRenderDispatcherMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/shadows/EntityRenderDispatcherMixin.java index 7f96b527b3..178f8990c3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/shadows/EntityRenderDispatcherMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/entity/shadows/EntityRenderDispatcherMixin.java @@ -37,7 +37,7 @@ public class EntityRenderDispatcherMixin { */ @Inject(method = "renderBlockShadow", at = @At("HEAD"), cancellable = true) private static void renderShadowPartFast(PoseStack.Pose matrices, VertexConsumer vertices, ChunkAccess chunk, LevelReader level, BlockPos pos, double x, double y, double z, float radius, float opacity, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertices); + var writer = VertexConsumerUtils.convertOrLog(vertices, EntityVertex.FORMAT); if (writer == null) { return; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ItemRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ItemRendererMixin.java index a9d7ab6b83..834ddec25b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ItemRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ItemRendererMixin.java @@ -50,12 +50,12 @@ public abstract class ItemRendererMixin { private final ItemRenderContext.VanillaModelBufferer vanillaBufferer = this::renderModelLists; @Unique - private final ThreadLocal contexts = ThreadLocal.withInitial(() -> new ItemRenderContext(itemColors, vanillaBufferer)); + private final ThreadLocal contexts = ThreadLocal.withInitial(() -> new ItemRenderContext(this.itemColors, this.vanillaBufferer)); @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BakedModel;isCustomRenderer()Z"), cancellable = true) private void beforeRenderItem(ItemStack stack, ItemDisplayContext transformMode, boolean invert, PoseStack matrixStack, MultiBufferSource vertexConsumerProvider, int light, int overlay, BakedModel model, CallbackInfo ci) { if (!((FabricBakedModel) model).isVanillaAdapter()) { - contexts.get().renderModel(stack, transformMode, invert, matrixStack, vertexConsumerProvider, light, overlay, model); + this.contexts.get().renderModel(stack, transformMode, invert, matrixStack, vertexConsumerProvider, light, overlay, model); matrixStack.popPose(); ci.cancel(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ModelBlockRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ModelBlockRendererMixin.java index e231e6497a..a0dd842782 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ModelBlockRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/frapi/ModelBlockRendererMixin.java @@ -45,12 +45,12 @@ public abstract class ModelBlockRendererMixin { private BlockColors blockColors; @Unique - private final ThreadLocal contexts = ThreadLocal.withInitial(() -> new NonTerrainBlockRenderContext(blockColors)); + private final ThreadLocal contexts = ThreadLocal.withInitial(() -> new NonTerrainBlockRenderContext(this.blockColors)); @Inject(method = "tesselateBlock", at = @At("HEAD"), cancellable = true) private void onRender(BlockAndTintGetter blockView, BakedModel model, BlockState state, BlockPos pos, PoseStack matrix, VertexConsumer buffer, boolean cull, RandomSource rand, long seed, int overlay, CallbackInfo ci) { if (!((FabricBakedModel) model).isVanillaAdapter()) { - contexts.get().renderModel(blockView, model, state, pos, matrix, buffer, cull, rand, seed, overlay); + this.contexts.get().renderModel(blockView, model, state, pos, matrix, buffer, cull, rand, seed, overlay); ci.cancel(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/font/BakedGlyphMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/font/BakedGlyphMixin.java index 08e138f1e0..96f9fb006b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/font/BakedGlyphMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/font/BakedGlyphMixin.java @@ -53,7 +53,7 @@ public class BakedGlyphMixin { */ @Inject(method = "render", at = @At("HEAD"), cancellable = true) private void drawFast(boolean italic, float x, float y, Matrix4f matrix, VertexConsumer vertexConsumer, float red, float green, float blue, float alpha, int light, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, GlyphVertex.FORMAT); if (writer == null) { return; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/outlines/LevelRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/outlines/LevelRendererMixin.java index e78a553301..106e28a386 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/outlines/LevelRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/gui/outlines/LevelRendererMixin.java @@ -29,7 +29,7 @@ public class LevelRendererMixin { private static void drawBoxFast(PoseStack matrices, VertexConsumer vertexConsumer, double x1, double y1, double z1, double x2, double y2, double z2, float red, float green, float blue, float alpha, float xAxisRed, float yAxisGreen, float zAxisBlue, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, LineVertex.FORMAT); if (writer == null) { return; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderAccessor.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderAccessor.java new file mode 100644 index 0000000000..f96041566e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderAccessor.java @@ -0,0 +1,11 @@ +package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.intrinsics; + +import com.mojang.blaze3d.vertex.BufferBuilder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(BufferBuilder.class) +public interface BufferBuilderAccessor { + @Accessor("fastFormat") + boolean sodium$fastFormat(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderMixin.java deleted file mode 100644 index a72f45f5bc..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/BufferBuilderMixin.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.intrinsics; - -import com.mojang.blaze3d.vertex.BufferBuilder; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; -import net.caffeinemc.mods.sodium.client.render.immediate.model.BakedModelEncoder; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; -import net.caffeinemc.mods.sodium.api.util.ColorABGR; -import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; -import net.minecraft.client.renderer.block.model.BakedQuad; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -@SuppressWarnings({ "SameParameterValue" }) -@Mixin(BufferBuilder.class) -public abstract class BufferBuilderMixin implements VertexConsumer { - @Shadow - @Final - private boolean fastFormat; - - @Override - public void putBulkData(PoseStack.Pose matrices, BakedQuad bakedQuad, float r, float g, float b, float a, int light, int overlay) { - if (!this.fastFormat) { - VertexConsumer.super.putBulkData(matrices, bakedQuad, r, g, b, a, light, overlay); - - SpriteUtil.markSpriteActive(bakedQuad.getSprite()); - - return; - } - - if (bakedQuad.getVertices().length < 32) { - return; // we do not accept quads with less than 4 properly sized vertices - } - - VertexBufferWriter writer = VertexBufferWriter.of(this); - - ModelQuadView quad = (ModelQuadView) bakedQuad; - - int color = ColorABGR.pack(r, g, b, a); - BakedModelEncoder.writeQuadVertices(writer, matrices, quad, color, light, overlay, false); - - SpriteUtil.markSpriteActive(quad.getSprite()); - } - - @Override - public void putBulkData(PoseStack.Pose matrices, BakedQuad bakedQuad, float[] brightnessTable, float r, float g, float b, float a, int[] light, int overlay, boolean colorize) { - if (!this.fastFormat) { - VertexConsumer.super.putBulkData(matrices, bakedQuad, brightnessTable, r, g, b, a, light, overlay, colorize); - - SpriteUtil.markSpriteActive(bakedQuad.getSprite()); - - return; - } - - if (bakedQuad.getVertices().length < 32) { - return; // we do not accept quads with less than 4 properly sized vertices - } - - VertexBufferWriter writer = VertexBufferWriter.of(this); - - ModelQuadView quad = (ModelQuadView) bakedQuad; - - BakedModelEncoder.writeQuadVertices(writer, matrices, quad, r, g, b, a, brightnessTable, colorize, light, overlay); - - SpriteUtil.markSpriteActive(quad.getSprite()); - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/VertexConsumerMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/VertexConsumerMixin.java new file mode 100644 index 0000000000..22867435ec --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/intrinsics/VertexConsumerMixin.java @@ -0,0 +1,80 @@ +package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.intrinsics; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; +import net.caffeinemc.mods.sodium.client.render.immediate.model.BakedModelEncoder; +import net.minecraft.client.renderer.block.model.BakedQuad; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(VertexConsumer.class) +public interface VertexConsumerMixin { + @WrapMethod(method = "putBulkData(Lcom/mojang/blaze3d/vertex/PoseStack$Pose;Lnet/minecraft/client/renderer/block/model/BakedQuad;FFFFII)V") + default void sodium$modifyPutBulkData(PoseStack.Pose pose, BakedQuad bakedQuad, float red, float green, float blue, float alpha, int packedLight, int packedOverlay, Operation original) { + if ((Object) this instanceof BufferBuilder bufferBuilder) { + if (!((BufferBuilderAccessor) bufferBuilder).sodium$fastFormat()) { + original.call(pose, bakedQuad, red, green, blue, alpha, packedLight, packedOverlay); + + if (bakedQuad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(bakedQuad.getSprite()); + } + + return; + } + + if (bakedQuad.getVertices().length < 32) { + return; // we do not accept quads with less than 4 properly sized vertices + } + + VertexBufferWriter writer = VertexBufferWriter.of((VertexConsumer) this); + + ModelQuadView quad = (ModelQuadView) bakedQuad; + + int color = ColorABGR.pack(red, green, blue, alpha); + BakedModelEncoder.writeQuadVertices(writer, pose, quad, color, packedLight, packedOverlay, false); + + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } + } else { + original.call(pose, bakedQuad, red, green, blue, alpha, packedLight, packedOverlay); + } + } + + @WrapMethod(method = "putBulkData(Lcom/mojang/blaze3d/vertex/PoseStack$Pose;Lnet/minecraft/client/renderer/block/model/BakedQuad;[FFFFF[IIZ)V") + default void sodium$modifyPutBulkData(PoseStack.Pose pose, BakedQuad bakedQuad, float[] brightness, float red, float green, float blue, float alpha, int[] lightmap, int packedOverlay, boolean readAlpha, Operation original) { + if ((Object) this instanceof BufferBuilder bufferBuilder) { + if (!((BufferBuilderAccessor) bufferBuilder).sodium$fastFormat()) { + original.call(pose, bakedQuad, brightness, red, green, blue, alpha, lightmap, packedOverlay, readAlpha); + + if (bakedQuad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(bakedQuad.getSprite()); + } + + return; + } + + if (bakedQuad.getVertices().length < 32) { + return; // we do not accept quads with less than 4 properly sized vertices + } + + VertexBufferWriter writer = VertexBufferWriter.of((VertexConsumer) this); + + ModelQuadView quad = (ModelQuadView) bakedQuad; + + BakedModelEncoder.writeQuadVertices(writer, pose, quad, red, green, blue, alpha, brightness, readAlpha, lightmap, packedOverlay); + + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } + } else { + original.call(pose, bakedQuad, brightness, red, green, blue, alpha, lightmap, packedOverlay, readAlpha); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataAccessor.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataAccessor.java new file mode 100644 index 0000000000..dbb4d47cc8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataAccessor.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.sorting; + +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MeshData.class) +public interface MeshDataAccessor { + @Accessor("indexBuffer") + void sodium$setIndexBuffer(ByteBufferBuilder.Result buffer); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataMixin.java deleted file mode 100644 index d05d3a9f06..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MeshDataMixin.java +++ /dev/null @@ -1,47 +0,0 @@ - -package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.sorting; - -import com.mojang.blaze3d.vertex.MeshData; -import org.jetbrains.annotations.Nullable; -import org.joml.Vector3f; -import org.lwjgl.system.MemoryUtil; -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 com.mojang.blaze3d.vertex.BufferBuilder; -import com.mojang.blaze3d.vertex.VertexFormat; -import com.mojang.blaze3d.vertex.VertexSorting; -import java.nio.ByteBuffer; - -@Mixin(MeshData.class) -public abstract class MeshDataMixin { - /** - * @author JellySquid - * @reason Avoid slow memory accesses - */ - @Overwrite - private static Vector3f[] unpackQuadCentroids(ByteBuffer buffer, int vertices, VertexFormat format) { - int vertexStride = format.getVertexSize(); - int primitiveCount = vertices / 4; - - Vector3f[] centers = new Vector3f[primitiveCount]; - - for (int index = 0; index < primitiveCount; ++index) { - long v1 = MemoryUtil.memAddress(buffer, (((index * 4) + 0) * vertexStride)); - long v2 = MemoryUtil.memAddress(buffer, (((index * 4) + 2) * vertexStride)); - - float x1 = MemoryUtil.memGetFloat(v1 + 0); - float y1 = MemoryUtil.memGetFloat(v1 + 4); - float z1 = MemoryUtil.memGetFloat(v1 + 8); - - float x2 = MemoryUtil.memGetFloat(v2 + 0); - float y2 = MemoryUtil.memGetFloat(v2 + 4); - float z2 = MemoryUtil.memGetFloat(v2 + 8); - - centers[index] = new Vector3f((x1 + x2) * 0.5F, (y1 + y2) * 0.5F, (z1 + z2) * 0.5F); - } - - return centers; - } -} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MultiBufferSourceMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MultiBufferSourceMixin.java new file mode 100644 index 0000000000..a65aa9cf74 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/MultiBufferSourceMixin.java @@ -0,0 +1,96 @@ +package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.sorting; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexSorting; +import net.caffeinemc.mods.sodium.client.util.sorting.VertexSorters; +import net.caffeinemc.mods.sodium.client.util.sorting.VertexSortingExtended; +import net.minecraft.client.renderer.MultiBufferSource; +import org.lwjgl.system.MemoryUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MultiBufferSource.BufferSource.class) +public class MultiBufferSourceMixin { + @Unique + private static final int VERTICES_PER_QUAD = 6; + + @WrapOperation( + method = "endBatch(Lnet/minecraft/client/renderer/RenderType;Lcom/mojang/blaze3d/vertex/BufferBuilder;)V", + at = @At( + value = "INVOKE", + target = "Lcom/mojang/blaze3d/vertex/MeshData;sortQuads(Lcom/mojang/blaze3d/vertex/ByteBufferBuilder;Lcom/mojang/blaze3d/vertex/VertexSorting;)Lcom/mojang/blaze3d/vertex/MeshData$SortState;" + ) + ) + private MeshData.SortState redirectSortQuads(MeshData meshData, ByteBufferBuilder bufferBuilder, VertexSorting sorting, Operation original) { + if (sorting instanceof VertexSortingExtended sortingExtended) { + // Replace the vertex sorting algorithm when it implements our accelerated sort. + acceleratedSort(meshData, bufferBuilder, sortingExtended); + } else { + return original.call(meshData, bufferBuilder, sorting); + } + + // The caller never uses the return value. + return null; + } + + @Unique + private static void acceleratedSort(MeshData meshData, ByteBufferBuilder bufferBuilder, VertexSortingExtended sorting) { + final var drawState = meshData.drawState(); + + if (drawState.mode() != VertexFormat.Mode.QUADS) { + // Only quad lists can be sorted. + return; + } + + var sortedPrimitiveIds = VertexSorters.sort(meshData.vertexBuffer(), drawState.vertexCount(), drawState.format().getVertexSize(), sorting); + var sortedIndexBuffer = buildSortedIndexBuffer(meshData, bufferBuilder, sortedPrimitiveIds); + ((MeshDataAccessor) meshData).sodium$setIndexBuffer(sortedIndexBuffer); + } + + @Unique + private static ByteBufferBuilder.Result buildSortedIndexBuffer(MeshData meshData, ByteBufferBuilder bufferBuilder, int[] primitiveIds) { + final var indexType = meshData.drawState().indexType(); + final var ptr = bufferBuilder.reserve((primitiveIds.length * VERTICES_PER_QUAD) * indexType.bytes); + + if (indexType == VertexFormat.IndexType.SHORT) { + writeIndexBufferShort(ptr, primitiveIds); + } else if (indexType == VertexFormat.IndexType.INT) { + writeIndexBufferInt(ptr, primitiveIds); + } else { + throw new UnsupportedOperationException(); + } + + return bufferBuilder.build(); + } + + @Unique + private static void writeIndexBufferInt(long ptr, int[] primitiveIds) { + for (int primitiveId : primitiveIds) { + MemoryUtil.memPutInt(ptr + 0L, (primitiveId * 4) + 0); + MemoryUtil.memPutInt(ptr + 4L, (primitiveId * 4) + 1); + MemoryUtil.memPutInt(ptr + 8L, (primitiveId * 4) + 2); + MemoryUtil.memPutInt(ptr + 12L, (primitiveId * 4) + 2); + MemoryUtil.memPutInt(ptr + 16L, (primitiveId * 4) + 3); + MemoryUtil.memPutInt(ptr + 20L, (primitiveId * 4) + 0); + ptr += 24L; + } + } + + @Unique + private static void writeIndexBufferShort(long ptr, int[] primitiveIds) { + for (int primitiveId : primitiveIds) { + MemoryUtil.memPutShort(ptr + 0L, (short) ((primitiveId * 4) + 0)); + MemoryUtil.memPutShort(ptr + 2L, (short) ((primitiveId * 4) + 1)); + MemoryUtil.memPutShort(ptr + 4L, (short) ((primitiveId * 4) + 2)); + MemoryUtil.memPutShort(ptr + 6L, (short) ((primitiveId * 4) + 2)); + MemoryUtil.memPutShort(ptr + 8L, (short) ((primitiveId * 4) + 3)); + MemoryUtil.memPutShort(ptr + 10L, (short) ((primitiveId * 4) + 0)); + ptr += 12L; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/VertexSortingMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/VertexSortingMixin.java index fddfd57503..98b719ab37 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/VertexSortingMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/immediate/buffer_builder/sorting/VertexSortingMixin.java @@ -1,19 +1,42 @@ package net.caffeinemc.mods.sodium.mixin.features.render.immediate.buffer_builder.sorting; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.mojang.blaze3d.vertex.VertexSorting; import net.caffeinemc.mods.sodium.client.util.sorting.VertexSorters; -import org.joml.Vector3f; +import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.injection.At; @Mixin(VertexSorting.class) public interface VertexSortingMixin { + @SuppressWarnings("DiscouragedShift") // Not currently avoidable. + @ModifyExpressionValue( + method = "", + at = @At( + value = "FIELD", + target = "Lcom/mojang/blaze3d/vertex/VertexSorting;ORTHOGRAPHIC_Z:Lcom/mojang/blaze3d/vertex/VertexSorting;", + opcode = Opcodes.PUTSTATIC, + shift = At.Shift.BEFORE)) + private static VertexSorting modifyVertexSorting(VertexSorting original) { + return VertexSorters.orthographicZ(); + } + /** * @author JellySquid * @reason Optimize vertex sorting */ @Overwrite static VertexSorting byDistance(float x, float y, float z) { - return VertexSorters.sortByDistance(new Vector3f(x, y, z)); + return VertexSorters.distance(x, y, z); + } + + /** + * @author JellySquid + * @reason Optimize vertex sorting + */ + @Overwrite + static VertexSorting byDistance(VertexSorting.DistanceFunction function) { + return VertexSorters.fallback(function); } -} +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/item/ItemRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/item/ItemRendererMixin.java index a3b15c4252..8a764860ec 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/item/ItemRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/item/ItemRendererMixin.java @@ -1,28 +1,30 @@ package net.caffeinemc.mods.sodium.mixin.features.render.model.item; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.util.ColorARGB; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; import net.caffeinemc.mods.sodium.client.model.quad.BakedQuadView; import net.caffeinemc.mods.sodium.client.render.immediate.model.BakedModelEncoder; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils; -import net.caffeinemc.mods.sodium.client.model.color.interop.ItemColorsExtension; import net.caffeinemc.mods.sodium.client.util.DirectionUtil; -import net.caffeinemc.mods.sodium.api.util.ColorARGB; -import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; -import net.minecraft.client.color.item.ItemColor; import net.minecraft.client.color.item.ItemColors; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.client.renderer.entity.ItemRenderer; -import net.minecraft.client.resources.model.BakedModel; import net.minecraft.core.Direction; import net.minecraft.util.RandomSource; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.levelgen.SingleThreadedRandomSource; -import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.Final; +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 com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; + import java.util.List; @Mixin(ItemRenderer.class) @@ -35,48 +37,45 @@ public class ItemRendererMixin { private ItemColors itemColors; /** - * @reason Avoid allocations - * @author JellySquid + * @reason Avoid Allocations + * @return JellySquid */ - @Inject(method = "renderModelLists", at = @At("HEAD"), cancellable = true) - private void renderModelFast(BakedModel model, ItemStack itemStack, int light, int overlay, PoseStack matrixStack, VertexConsumer vertexConsumer, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); - - if (writer == null) { - return; - } - - ci.cancel(); - - RandomSource random = this.random; - PoseStack.Pose matrices = matrixStack.last(); - - ItemColor colorProvider = null; + @WrapOperation(method = "renderModelLists", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;create()Lnet/minecraft/util/RandomSource;")) + private RandomSource renderModelFastRandom(Operation original) { + return this.random; + } - if (!itemStack.isEmpty()) { - colorProvider = ((ItemColorsExtension) this.itemColors).sodium$getColorProvider(itemStack); - } + /** + * @reason Avoid Allocations + * @return JellySquid + */ + @WrapOperation(method = "renderModelLists", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/Direction;values()[Lnet/minecraft/core/Direction;")) + private Direction[] renderModelFastDirections(Operation original) { + return DirectionUtil.ALL_DIRECTIONS; + } - for (Direction direction : DirectionUtil.ALL_DIRECTIONS) { - random.setSeed(42L); - List quads = model.getQuads(null, direction, random); + /** + * @reason Avoid Allocations + * @return JellySquid + */ + @WrapOperation(method = "renderModelLists", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/ItemRenderer;renderQuadList(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Ljava/util/List;Lnet/minecraft/world/item/ItemStack;II)V")) + private void renderModelFast(ItemRenderer itemRenderer, PoseStack poseStack, VertexConsumer vertexConsumer, List quads, ItemStack itemStack, int light, int overlay, Operation original) { + var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, EntityVertex.FORMAT); - if (!quads.isEmpty()) { - this.renderBakedItemQuads(matrices, writer, quads, itemStack, colorProvider, light, overlay); - } + if (writer == null) { + original.call(itemRenderer, poseStack, vertexConsumer, quads, itemStack, light, overlay); + return; } - random.setSeed(42L); - List quads = model.getQuads(null, null, random); - + // TODO/NOTE: Should .last be a LocalRef? if (!quads.isEmpty()) { - this.renderBakedItemQuads(matrices, writer, quads, itemStack, colorProvider, light, overlay); + this.renderBakedItemQuads(poseStack.last(), writer, quads, itemStack, light, overlay); } } @Unique @SuppressWarnings("ForLoopReplaceableByForEach") - private void renderBakedItemQuads(PoseStack.Pose matrices, VertexBufferWriter writer, List quads, ItemStack itemStack, ItemColor colorProvider, int light, int overlay) { + private void renderBakedItemQuads(PoseStack.Pose matrices, VertexBufferWriter writer, List quads, ItemStack itemStack, int light, int overlay) { for (int i = 0; i < quads.size(); i++) { BakedQuad bakedQuad = quads.get(i); @@ -88,13 +87,15 @@ private void renderBakedItemQuads(PoseStack.Pose matrices, VertexBufferWriter wr int color = 0xFFFFFFFF; - if (colorProvider != null && quad.hasColor()) { - color = ColorARGB.toABGR((colorProvider.getColor(itemStack, quad.getColorIndex()))); + if (quad.hasColor()) { + color = ColorARGB.toABGR((this.itemColors.getColor(itemStack, quad.getColorIndex()))); } BakedModelEncoder.writeQuadVertices(writer, matrices, quad, color, light, overlay, BakedModelEncoder.shouldMultiplyAlpha()); - SpriteUtil.markSpriteActive(quad.getSprite()); + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/particle/SingleQuadParticleMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/particle/SingleQuadParticleMixin.java index 6dd1a3e2cc..8957e71146 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/particle/SingleQuadParticleMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/particle/SingleQuadParticleMixin.java @@ -1,12 +1,17 @@ package net.caffeinemc.mods.sodium.mixin.features.render.particle; -import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex; import com.mojang.blaze3d.vertex.VertexConsumer; import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex; import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils; +import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Math; import org.joml.Quaternionf; import org.joml.Vector3f; import org.lwjgl.system.MemoryStack; @@ -38,13 +43,19 @@ protected SingleQuadParticleMixin(ClientLevel level, double x, double y, double super(level, x, y, z); } + @Unique + private static final Vector3f TEMP_LEFT = new Vector3f(); + + @Unique + private static final Vector3f TEMP_UP = new Vector3f(); + /** - * @reason Optimize function - * @author JellySquid + * @reason Build vertex data using the left and up vectors to avoid quaternion calculations + * @author MoePus */ - @Inject(method = "renderRotatedQuad(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lorg/joml/Quaternionf;FFFF)V", at = @At("HEAD"), cancellable = true) - protected void renderRotatedQuad(VertexConsumer vertexConsumer, Quaternionf quaternionf, float x, float y, float z, float tickDelta, CallbackInfo ci) { - final var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + @Inject(method = "render(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/client/Camera;F)V", at = @At("HEAD"), cancellable = true) + protected void render(VertexConsumer vertexConsumer, Camera camera, float tickDelta, CallbackInfo ci) { + final var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, ParticleVertex.FORMAT); if (writer == null) { return; @@ -53,45 +64,97 @@ protected void renderRotatedQuad(VertexConsumer vertexConsumer, Quaternionf quat ci.cancel(); float size = this.getQuadSize(tickDelta); + + Vector3f left = TEMP_LEFT; + left.set(camera.getLeftVector()) + .mul(size); + + Vector3f up = TEMP_UP; + up.set(camera.getUpVector()) + .mul(size); + + if (!Mth.equal(this.roll, 0.0f)) { + float roll = Mth.lerp(tickDelta, this.oRoll, this.roll); + + float sinRoll = Math.sin(roll); + float cosRoll = Math.cosFromSin(sinRoll, roll); + + float rv1x = Math.fma(cosRoll, left.x, sinRoll * up.x), + rv1y = Math.fma(cosRoll, left.y, sinRoll * up.y), + rv1z = Math.fma(cosRoll, left.z, sinRoll * up.z); + + float rv2x = Math.fma(-sinRoll, left.x, cosRoll * up.x), + rv2y = Math.fma(-sinRoll, left.y, cosRoll * up.y), + rv2z = Math.fma(-sinRoll, left.z, cosRoll * up.z); + + left.set(rv1x, rv1y, rv1z); + up.set(rv2x, rv2y, rv2z); + } + + this.sodium$emitVertices(writer, camera.getPosition(), left, up, tickDelta); + } + + /** + * @reason Build vertex data using the left and up vectors to avoid quaternion calculations + * @author MoePus + */ + @Inject(method = "renderRotatedQuad(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/client/Camera;Lorg/joml/Quaternionf;F)V", at = @At("HEAD"), cancellable = true) + protected void renderRotatedQuad(VertexConsumer vertexConsumer, Camera camera, Quaternionf quaternion, float tickDelta, CallbackInfo ci) { + final var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, ParticleVertex.FORMAT); + + if (writer == null) { + return; + } + + ci.cancel(); + + float size = this.getQuadSize(tickDelta); + + // Some particle class implementations may call this function directly, in which case we cannot assume anything + // about the transform being used. However, we can still extract the left/up vectors from the quaternion, + // it's just slightly slower than using the camera's left/up vectors directly. + Vector3f left = TEMP_LEFT; + left.set(-size, 0.0f, 0.0f) + .rotate(quaternion); + + Vector3f up = TEMP_UP; + up.set(0.0f, size, 0.0f) + .rotate(quaternion); + + this.sodium$emitVertices(writer, camera.getPosition(), left, up, tickDelta); + } + + @Unique + private void sodium$emitVertices(VertexBufferWriter writer, Vec3 camera, Vector3f left, Vector3f up, float tickDelta) { float minU = this.getU0(); float maxU = this.getU1(); float minV = this.getV0(); float maxV = this.getV1(); - int light = this.getLightColor(tickDelta); + int light = this.getLightColor(tickDelta); int color = ColorABGR.pack(this.rCol, this.gCol, this.bCol, this.alpha); + float x = (float) (Mth.lerp(tickDelta, this.xo, this.x) - camera.x()); + float y = (float) (Mth.lerp(tickDelta, this.yo, this.y) - camera.y()); + float z = (float) (Mth.lerp(tickDelta, this.zo, this.z) - camera.z()); + try (MemoryStack stack = MemoryStack.stackPush()) { long buffer = stack.nmalloc(4 * ParticleVertex.STRIDE); long ptr = buffer; - this.sodium$writeVertex(ptr, quaternionf, x, y, z, 1.0F, -1.0F, size, maxU, maxV, color, light); + ParticleVertex.put(ptr, -left.x - up.x + x, -left.y - up.y + y, -left.z - up.z + z, maxU, maxV, color, light); ptr += ParticleVertex.STRIDE; - this.sodium$writeVertex(ptr, quaternionf, x, y, z, 1.0F, 1.0F, size, maxU, minV, color, light); + ParticleVertex.put(ptr, -left.x + up.x + x, -left.y + up.y + y, -left.z + up.z + z, maxU, minV, color, light); ptr += ParticleVertex.STRIDE; - this.sodium$writeVertex(ptr, quaternionf, x, y, z, -1.0F, 1.0F, size, minU, minV, color, light); + ParticleVertex.put(ptr, left.x + up.x + x, left.y + up.y + y, left.z + up.z + z, minU, minV, color, light); ptr += ParticleVertex.STRIDE; - this.sodium$writeVertex(ptr, quaternionf, x, y, z, -1.0F, -1.0F, size, minU, maxV, color, light); + ParticleVertex.put(ptr, left.x - up.x + x, left.y - up.y + y, left.z - up.z + z, minU, maxV, color, light); ptr += ParticleVertex.STRIDE; writer.push(stack, buffer, 4, ParticleVertex.FORMAT); } } - - @Unique - private final Vector3f sodium$scratchVertex = new Vector3f(); // not thread-safe - - @Unique - private void sodium$writeVertex(long ptr, Quaternionf quaternionf, float originX, float originY, float originZ, float posX, float posY, float size, float u, float v, int color, int light) { - final var vertex = this.sodium$scratchVertex; - vertex.set(posX, posY, 0.0f); - vertex.rotate(quaternionf); - vertex.mul(size); - vertex.add(originX, originY, originZ); - - ParticleVertex.put(ptr, vertex.x(), vertex.y(), vertex.z(), u, v, color, light); - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/viewport/GlStateManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/viewport/GlStateManagerMixin.java new file mode 100644 index 0000000000..b76ea06498 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/viewport/GlStateManagerMixin.java @@ -0,0 +1,34 @@ +package net.caffeinemc.mods.sodium.mixin.features.render.viewport; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.mojang.blaze3d.platform.GlStateManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GlStateManager.class) +public class GlStateManagerMixin { + @Unique + private static int lastViewportX; + @Unique + private static int lastViewportY; + @Unique + private static int lastViewportWidth; + @Unique + private static int lastViewportHeight; + + @WrapWithCondition( + method = "_viewport", + at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glViewport(IIII)V") + ) + private static boolean skipRedundantViewport(int x, int y, int w, int h) { + if (x == lastViewportX && y == lastViewportY && w == lastViewportWidth && h == lastViewportHeight) { + return false; + } + lastViewportX = x; + lastViewportY = y; + lastViewportWidth = w; + lastViewportHeight = h; + return true; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/clouds/LevelRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/clouds/LevelRendererMixin.java index a5047b7122..a3c589a5b3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/clouds/LevelRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/clouds/LevelRendererMixin.java @@ -56,7 +56,7 @@ public void renderClouds(PoseStack poseStack, Matrix4f matrix4f, Matrix4f projec @Inject(method = "onResourceManagerReload(Lnet/minecraft/server/packs/resources/ResourceManager;)V", at = @At("RETURN")) private void onReload(ResourceManager manager, CallbackInfo ci) { if (this.cloudRenderer != null) { - this.cloudRenderer.reloadTextures(manager); + this.cloudRenderer.reload(manager); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/sky/LevelRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/sky/LevelRendererMixin.java index 4edcbc806d..11e29c7adc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/sky/LevelRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/world/sky/LevelRendererMixin.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.mixin.features.render.world.sky; -import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Camera; import net.minecraft.client.renderer.FogRenderer; import net.minecraft.client.renderer.LevelRenderer; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/shader/uniform/ShaderInstanceMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/shader/uniform/ShaderInstanceMixin.java index 4c37086f9f..1ea5656385 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/shader/uniform/ShaderInstanceMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/shader/uniform/ShaderInstanceMixin.java @@ -1,9 +1,9 @@ package net.caffeinemc.mods.sodium.mixin.features.shader.uniform; import com.mojang.blaze3d.shaders.Uniform; -import com.mojang.blaze3d.vertex.VertexFormat; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.client.renderer.ShaderInstance; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -14,8 +14,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.List; -import net.minecraft.client.renderer.ShaderInstance; -import net.minecraft.server.packs.resources.ResourceProvider; /** * On the NVIDIA drivers (and maybe some others), the OpenGL submission thread requires expensive state synchronization diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/NativeImageAccessor.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/NativeImageAccessor.java index 325176fded..08e3342c7d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/NativeImageAccessor.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/NativeImageAccessor.java @@ -6,6 +6,6 @@ @Mixin(NativeImage.class) public interface NativeImageAccessor { - @Accessor - long getPixels(); + @Accessor("pixels") + long sodium$getPixels(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/GuiGraphicsMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/GuiGraphicsMixin.java index 49d71efb44..fed72a54e8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/GuiGraphicsMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/GuiGraphicsMixin.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.mixin.features.textures.animations.tracking; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import org.spongepowered.asm.mixin.Mixin; @@ -16,7 +16,7 @@ private void preDrawSprite(int x, int y, int z, TextureAtlasSprite sprite, CallbackInfo ci) { - SpriteUtil.markSpriteActive(sprite); + SpriteUtil.INSTANCE.markSpriteActive(sprite); } @Inject(method = "blit(IIIIILnet/minecraft/client/renderer/texture/TextureAtlasSprite;FFFF)V", at = @At("HEAD")) @@ -26,6 +26,6 @@ private void preDrawSprite(int x, int y, int z, float red, float green, float blue, float alpha, CallbackInfo ci) { - SpriteUtil.markSpriteActive(sprite); + SpriteUtil.INSTANCE.markSpriteActive(sprite); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/ModelBlockRendererMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/ModelBlockRendererMixin.java index 020ee3cb27..4444d4c71c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/ModelBlockRendererMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/ModelBlockRendererMixin.java @@ -2,7 +2,7 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.minecraft.client.renderer.block.ModelBlockRenderer; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.core.BlockPos; @@ -22,6 +22,8 @@ public class ModelBlockRendererMixin { */ @Inject(method = "putQuadData", at = @At("HEAD")) private void preRenderQuad(BlockAndTintGetter level, BlockState state, BlockPos pos, VertexConsumer vertexConsumer, PoseStack.Pose matrices, BakedQuad quad, float brightness0, float brightness1, float brightness2, float brightness3, int light0, int light1, int light2, int light3, int overlay, CallbackInfo ci) { - SpriteUtil.markSpriteActive(quad.getSprite()); + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasMixin.java index d60038f2c4..f23ca8fe4f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasMixin.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.mixin.features.textures.animations.tracking; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import org.spongepowered.asm.mixin.Mixin; @@ -15,7 +15,7 @@ private void preReturnSprite(CallbackInfoReturnable cir) { TextureAtlasSprite sprite = cir.getReturnValue(); if (sprite != null) { - SpriteUtil.markSpriteActive(sprite); + SpriteUtil.INSTANCE.markSpriteActive(sprite); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasSpriteMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasSpriteMixin.java new file mode 100644 index 0000000000..3fa12457c7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureAtlasSpriteMixin.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.mixin.features.textures.animations.tracking; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TextureAtlasSprite.class) +public abstract class TextureAtlasSpriteMixin { + @Inject(method = "wrap", at = @At("HEAD")) + private void markSpriteAsActive(CallbackInfoReturnable cir) { + SpriteUtil.INSTANCE.markSpriteActive((TextureAtlasSprite) (Object) this); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureSheetParticleMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureSheetParticleMixin.java index 6773925be4..a4f162ab15 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureSheetParticleMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/animations/tracking/TextureSheetParticleMixin.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.mixin.features.textures.animations.tracking; import com.mojang.blaze3d.vertex.VertexConsumer; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.SingleQuadParticle; @@ -28,13 +28,13 @@ protected TextureSheetParticleMixin(ClientLevel level, double x, double y, doubl @Inject(method = "setSprite(Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)V", at = @At("RETURN")) private void afterSetSprite(TextureAtlasSprite sprite, CallbackInfo ci) { - this.shouldTickSprite = sprite != null && SpriteUtil.hasAnimation(sprite); + this.shouldTickSprite = sprite != null && SpriteUtil.INSTANCE.hasAnimation(sprite); } @Override public void render(VertexConsumer vertexConsumer, Camera camera, float tickDelta) { if (this.shouldTickSprite) { - SpriteUtil.markSpriteActive(this.sprite); + SpriteUtil.INSTANCE.markSpriteActive(this.sprite); } super.render(vertexConsumer, camera, tickDelta); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java index db106e65e4..686b21e199 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java @@ -35,7 +35,7 @@ public class SpriteContentsMixin implements SpriteContentsExtension { */ @WrapOperation(method = "", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD)) private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation original) { - scanSpriteContents(nativeImage); + this.scanSpriteContents(nativeImage); original.call(instance, nativeImage); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/TextureAtlasSpriteMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/TextureAtlasSpriteMixin.java new file mode 100644 index 0000000000..521c67bc60 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/TextureAtlasSpriteMixin.java @@ -0,0 +1,33 @@ +package net.caffeinemc.mods.sodium.mixin.features.textures.scan; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.TextureAtlasSpriteExtension; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteTicker; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(TextureAtlasSprite.class) +public class TextureAtlasSpriteMixin implements TextureAtlasSpriteExtension { + @Unique + private boolean hasUnknownImageContents; + + @WrapOperation(method = "createTicker", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;createTicker()Lnet/minecraft/client/renderer/texture/SpriteTicker;")) + private SpriteTicker hookTickerInstantiation(SpriteContents instance, Operation original) { + var ticker = original.call(instance); + + if (ticker != null && !(ticker instanceof SpriteContents.Ticker)) { + this.hasUnknownImageContents = true; + } + + return ticker; + } + + @Override + public boolean sodium$hasUnknownImageContents() { + return this.hasUnknownImageContents; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/workarounds/context_creation/WindowMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/workarounds/context_creation/WindowMixin.java index 6f78390690..4fd95b2f7b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/workarounds/context_creation/WindowMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/workarounds/context_creation/WindowMixin.java @@ -6,10 +6,10 @@ import com.mojang.blaze3d.platform.ScreenManager; import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.platform.WindowEventHandler; -import net.caffeinemc.mods.sodium.client.compatibility.checks.PostLaunchChecks; import net.caffeinemc.mods.sodium.client.compatibility.checks.ModuleScanner; -import net.caffeinemc.mods.sodium.client.gl.GlContextInfo; -import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.compatibility.checks.PostLaunchChecks; +import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd.AmdWorkarounds; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; @@ -38,59 +38,50 @@ public class WindowMixin { @Final private static Logger LOGGER; - @Shadow - @Final - private long window; - @Unique private long wglPrevContext = MemoryUtil.NULL; @Redirect(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwCreateWindow(IILjava/lang/CharSequence;JJ)J"), expect = 0, require = 0) private long wrapGlfwCreateWindow(int width, int height, CharSequence title, long monitor, long share) { - final boolean applyNvidiaWorkarounds = Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_THREADED_OPTIMIZATIONS); - - if (applyNvidiaWorkarounds) { - NvidiaWorkarounds.install(); - } + NvidiaWorkarounds.applyEnvironmentChanges(); + AmdWorkarounds.applyEnvironmentChanges(); try { return GLFW.glfwCreateWindow(width, height, title, monitor, share); } finally { - if (applyNvidiaWorkarounds) { - NvidiaWorkarounds.uninstall(); - } + NvidiaWorkarounds.undoEnvironmentChanges(); + AmdWorkarounds.undoEnvironmentChanges(); } } @SuppressWarnings("all") @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/loading/ImmediateWindowHandler;setupMinecraftWindow(Ljava/util/function/IntSupplier;Ljava/util/function/IntSupplier;Ljava/util/function/Supplier;Ljava/util/function/LongSupplier;)J"), expect = 0, require = 0) private long wrapGlfwCreateWindowForge(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitor, Operation op) { - final boolean applyNvidiaWorkarounds = Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_THREADED_OPTIMIZATIONS); + boolean applyWorkaroundsLate = !PlatformRuntimeInformation.getInstance() + .platformHasEarlyLoadingScreen(); - if (applyNvidiaWorkarounds && !PlatformRuntimeInformation.getInstance().platformHasEarlyLoadingScreen()) { - NvidiaWorkarounds.install(); + if (applyWorkaroundsLate) { + NvidiaWorkarounds.applyEnvironmentChanges(); + AmdWorkarounds.applyEnvironmentChanges(); } try { return op.call(width, height, title, monitor); } finally { - if (applyNvidiaWorkarounds) { - NvidiaWorkarounds.uninstall(); + if (applyWorkaroundsLate) { + NvidiaWorkarounds.undoEnvironmentChanges(); + AmdWorkarounds.undoEnvironmentChanges(); } } } + @Inject(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL;createCapabilities()Lorg/lwjgl/opengl/GLCapabilities;", shift = At.Shift.AFTER)) private void postContextReady(WindowEventHandler eventHandler, ScreenManager monitorTracker, DisplayData settings, String videoMode, String title, CallbackInfo ci) { - GlContextInfo driver = GlContextInfo.create(); - - if (driver == null) { - LOGGER.warn("Could not retrieve identifying strings for OpenGL implementation"); - } else { - LOGGER.info("OpenGL Vendor: {}", driver.vendor()); - LOGGER.info("OpenGL Renderer: {}", driver.renderer()); - LOGGER.info("OpenGL Version: {}", driver.version()); - } + GlContextInfo context = GlContextInfo.create(); + LOGGER.info("OpenGL Vendor: {}", context.vendor()); + LOGGER.info("OpenGL Renderer: {}", context.renderer()); + LOGGER.info("OpenGL Version: {}", context.version()); // Capture the current WGL context so that we can detect it being replaced later. if (Util.getPlatform() == Util.OS.WINDOWS) { @@ -99,7 +90,7 @@ private void postContextReady(WindowEventHandler eventHandler, ScreenManager mon this.wglPrevContext = MemoryUtil.NULL; } - PostLaunchChecks.onContextInitialized(); + PostLaunchChecks.onContextInitialized((NativeWindowHandle) this, context); ModuleScanner.checkModules((NativeWindowHandle) this); } diff --git a/common/src/main/resources/assets/minecraft/models/block/bell_floor.json b/common/src/main/resources/assets/minecraft/models/block/bell_floor.json new file mode 100644 index 0000000000..3300cc2013 --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/bell_floor.json @@ -0,0 +1,43 @@ +{ + "textures": { + "bar": "block/dark_oak_planks", + "post": "block/stone", + "particle": "block/bell_bottom" + }, + "elements": [ + { + "from": [ 2, 13, 7 ], + "to": [ 14, 15, 9 ], + "faces": { + "north": { "uv": [ 2, 2, 14, 4 ], "texture": "#bar" }, + "south": { "uv": [ 2, 3, 14, 5 ], "texture": "#bar" }, + "up": { "uv": [ 2, 3, 14, 5 ], "texture": "#bar" }, + "down": { "uv": [ 2, 3, 14, 5 ], "texture": "#bar" } + } + }, + { + "from": [ 14, 0, 6 ], + "to": [ 16, 16, 10 ], + "faces": { + "north": { "uv": [ 0, 1, 2, 16 ], "texture": "#post" }, + "east": { "uv": [ 0, 1, 4, 16 ], "texture": "#post", "cullface": "east" }, + "south": { "uv": [ 0, 1, 2, 16 ], "texture": "#post" }, + "west": { "uv": [ 0, 1, 4, 16 ], "texture": "#post" }, + "up": { "uv": [ 0, 0, 2, 4 ], "texture": "#post", "cullface": "up" }, + "down": { "uv": [ 0, 0, 2, 4 ], "texture": "#post", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 6 ], + "to": [ 2, 16, 10 ], + "faces": { + "north": { "uv": [ 0, 1, 2, 16 ], "texture": "#post" }, + "east": { "uv": [ 0, 1, 4, 16 ], "texture": "#post" }, + "south": { "uv": [ 0, 1, 2, 16 ], "texture": "#post" }, + "west": { "uv": [ 0, 1, 4, 16 ], "texture": "#post", "cullface": "west" }, + "up": { "uv": [ 0, 0, 2, 4 ], "texture": "#post", "cullface": "up" }, + "down": { "uv": [ 0, 0, 2, 4 ], "texture": "#post", "cullface": "down" } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/models/block/brewing_stand.json b/common/src/main/resources/assets/minecraft/models/block/brewing_stand.json new file mode 100644 index 0000000000..172c7704f3 --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/brewing_stand.json @@ -0,0 +1,57 @@ +{ + "textures": { + "base": "block/brewing_stand_base", + "particle": "block/brewing_stand", + "stand": "block/brewing_stand" + }, + "elements": [ + { + "from": [ 7, 0, 7 ], + "to": [ 9, 14, 9 ], + "faces": { + "north": { "uv": [ 7, 2, 9, 16 ], "texture": "#stand" }, + "east": { "uv": [ 7, 2, 9, 16 ], "texture": "#stand" }, + "south": { "uv": [ 7, 2, 9, 16 ], "texture": "#stand" }, + "west": { "uv": [ 7, 2, 9, 16 ], "texture": "#stand" }, + "up": { "uv": [ 7, 7, 9, 9 ], "texture": "#stand" }, + "down": { "uv": [ 7, 7, 9, 9 ], "texture": "#stand", "cullface": "down" } + } + }, + { + "from": [ 9, 0, 5 ], + "to": [ 15, 2, 11 ], + "faces": { + "north": { "uv": [ 9, 14, 15, 16 ], "texture": "#base" }, + "east": { "uv": [ 5, 14, 11, 16 ], "texture": "#base" }, + "south": { "uv": [ 9, 14, 15, 16 ], "texture": "#base" }, + "west": { "uv": [ 5, 14, 11, 16 ], "texture": "#base" }, + "up": { "uv": [ 9, 5, 15, 11 ], "texture": "#base" }, + "down": { "uv": [ 9, 5, 15, 11 ], "texture": "#base", "cullface": "down" } + } + }, + { + "from": [ 1, 0, 1 ], + "to": [ 7, 2, 7 ], + "faces": { + "north": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "east": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "south": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "west": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "up": { "uv": [ 1, 1, 7, 7 ], "texture": "#base" }, + "down": { "uv": [ 1, 1, 7, 7 ], "texture": "#base", "cullface": "down" } + } + }, + { + "from": [ 1, 0, 9 ], + "to": [ 7, 2, 15 ], + "faces": { + "north": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "east": { "uv": [ 9, 14, 15, 16 ], "texture": "#base" }, + "south": { "uv": [ 1, 14, 7, 16 ], "texture": "#base" }, + "west": { "uv": [ 9, 14, 15, 16 ], "texture": "#base" }, + "up": { "uv": [ 1, 9, 7, 15 ], "texture": "#base" }, + "down": { "uv": [ 1, 9, 7, 15 ], "texture": "#base", "cullface": "down" } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/models/block/cauldron.json b/common/src/main/resources/assets/minecraft/models/block/cauldron.json new file mode 100644 index 0000000000..f1e0e005c2 --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/cauldron.json @@ -0,0 +1,214 @@ +{ + "ambientocclusion": false, + "textures": { + "top": "block/cauldron_top", + "particle": "block/cauldron_side", + "side": "block/cauldron_side", + "inside": "block/cauldron_inner", + "bottom": "block/cauldron_bottom" + }, + "elements": [ + { + "from": [ 0, 3, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "west" }, + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 16, 0 ], + "to": [ 14, 16, 2 ], + "faces": { + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 14, 16, 0 ], + "to": [ 16, 16, 14 ], + "faces": { + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 16, 14 ], + "to": [ 16, 16, 16 ], + "faces": { + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 0, 16, 2 ], + "to": [ 2, 16, 16 ], + "faces": { + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 16, 2 ], + "faces": { + "south": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 14, 4, 2 ], + "to": [ 14, 16, 14 ], + "faces": { + "west": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 14 ], + "to": [ 14, 16, 14 ], + "faces": { + "north": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 2, 16, 14 ], + "faces": { + "east": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 4, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 4, 3, 4 ], + "faces": { + "north": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "north" }, + "west": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 4, 3, 16 ], + "faces": { + "south": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 12, 0, 12 ], + "to": [ 16, 3, 16 ], + "faces": { + "east": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "south" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 3, 4 ], + "faces": { + "north": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "east" } + } + }, + { + "from": [ 14, 0, 2 ], + "to": [ 16, 3, 4 ], + "faces": { + "south": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "west": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 14, 12, 16, 14 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 2, 0, 0 ], + "to": [ 4, 3, 2 ], + "faces": { + "east": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "south": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 2, 14, 4, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 2, 3, 14 ], + "faces": { + "north": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "east": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 0, 2, 2, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 14 ], + "to": [ 14, 3, 16 ], + "faces": { + "north": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "west": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "down": { "uv": [ 12, 0, 14, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 0, 2 ], + "faces": { + "down": { "uv": [ 12, 14, 16, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 2, 0, 4 ], + "faces": { + "down": { "uv": [ 0, 12, 2, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 14 ], + "to": [ 4, 0, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 4, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 0, 16 ], + "faces": { + "down": { "uv": [ 14, 0, 16, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 14, 3, 2 ], + "faces": { + "south": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "west": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 0, 0, 2 ], + "to": [ 2, 3, 4 ], + "faces": { + "east": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "south": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 0, 14 ], + "to": [ 4, 3, 16 ], + "faces": { + "north": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "east": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 3, 14 ], + "faces": { + "north": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" }, + "west": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/models/block/composter.json b/common/src/main/resources/assets/minecraft/models/block/composter.json index 88af7aed12..0bccf7e940 100644 --- a/common/src/main/resources/assets/minecraft/models/block/composter.json +++ b/common/src/main/resources/assets/minecraft/models/block/composter.json @@ -1,9 +1,10 @@ { "parent": "minecraft:block/block", "textures": { - "rim": "block/composter_top", + "top": "block/composter_top", + "bottom": "block/composter_bottom", "particle": "block/composter_side", - "outside": "block/composter_side", + "side": "block/composter_side", "inside": "block/composter_bottom" }, "elements": [ @@ -11,67 +12,67 @@ "from": [ 0, 0, 0 ], "to": [ 16, 16, 16 ], "faces": { - "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#outside", "cullface": "north" }, - "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#outside", "cullface": "east" }, - "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#outside", "cullface": "south" }, - "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#outside", "cullface": "west" }, - "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside", "cullface": "down" } + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#side", "cullface": "west" }, + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#bottom", "cullface": "down" } } }, { "from": [ 0, 16, 0 ], "to": [ 14, 16, 2 ], "faces": { - "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 14, 16, 0 ], "to": [ 16, 16, 14 ], "faces": { - "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 16, 14 ], "to": [ 16, 16, 16 ], "faces": { - "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 0, 16, 2 ], "to": [ 2, 16, 16 ], "faces": { - "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 2, 2 ], "to": [ 14, 16, 2 ], "faces": { - "south": { "uv": [ 2, 0, 14, 14 ], "texture": "#outside" } + "south": { "uv": [ 2, 0, 14, 14 ], "texture": "#side" } } }, { "from": [ 14, 2, 2 ], "to": [ 14, 16, 14 ], "faces": { - "west": { "uv": [ 2, 0, 14, 14 ], "texture": "#outside" } + "west": { "uv": [ 2, 0, 14, 14 ], "texture": "#side" } } }, { "from": [ 2, 2, 14 ], "to": [ 14, 16, 14 ], "faces": { - "north": { "uv": [ 2, 0, 14, 14 ], "texture": "#outside" } + "north": { "uv": [ 2, 0, 14, 14 ], "texture": "#side" } } }, { "from": [ 2, 2, 2 ], "to": [ 2, 16, 14 ], "faces": { - "east": { "uv": [ 2, 0, 14, 14 ], "texture": "#outside" } + "east": { "uv": [ 2, 0, 14, 14 ], "texture": "#side" } } }, { diff --git a/common/src/main/resources/assets/minecraft/models/block/hopper.json b/common/src/main/resources/assets/minecraft/models/block/hopper.json index b1b655b3c9..a28b8f787e 100644 --- a/common/src/main/resources/assets/minecraft/models/block/hopper.json +++ b/common/src/main/resources/assets/minecraft/models/block/hopper.json @@ -1,9 +1,9 @@ { "ambientocclusion": false, "textures": { - "rim": "block/hopper_top", + "top": "block/hopper_top", "particle": "block/hopper_outside", - "outside": "block/hopper_outside", + "side": "block/hopper_outside", "inside": "block/hopper_inside" }, "elements": [ @@ -11,10 +11,10 @@ "from": [ 6, 0, 6 ], "to": [ 10, 4, 10 ], "faces": { - "north": { "uv": [ 6, 12, 10, 16 ], "texture": "#outside" }, - "east": { "uv": [ 6, 12, 10, 16 ], "texture": "#outside" }, - "south": { "uv": [ 6, 12, 10, 16 ], "texture": "#outside" }, - "west": { "uv": [ 6, 12, 10, 16 ], "texture": "#outside" }, + "north": { "uv": [ 6, 12, 10, 16 ], "texture": "#side" }, + "east": { "uv": [ 6, 12, 10, 16 ], "texture": "#side" }, + "south": { "uv": [ 6, 12, 10, 16 ], "texture": "#side" }, + "west": { "uv": [ 6, 12, 10, 16 ], "texture": "#side" }, "down": { "uv": [ 6, 6, 10, 10 ], "texture": "#inside", "cullface": "down" } } }, @@ -22,10 +22,10 @@ "from": [ 4, 4, 4 ], "to": [ 12, 10, 12 ], "faces": { - "north": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "east": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "south": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "west": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, + "north": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "east": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "south": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "west": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, "down": { "uv": [ 4, 4, 12, 12 ], "texture": "#inside" } } }, @@ -33,10 +33,10 @@ "from": [ 0, 10, 0 ], "to": [ 16, 16, 16 ], "faces": { - "north": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "north" }, - "east": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "east" }, - "south": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "south" }, - "west": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "west" }, + "north": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "west" }, "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } } }, @@ -44,56 +44,56 @@ "from": [ 0, 16, 0 ], "to": [ 14, 16, 2 ], "faces": { - "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 14, 16, 0 ], "to": [ 16, 16, 14 ], "faces": { - "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 16, 14 ], "to": [ 16, 16, 16 ], "faces": { - "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 0, 16, 2 ], "to": [ 2, 16, 16 ], "faces": { - "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 11, 2 ], "to": [ 14, 16, 2 ], "faces": { - "south": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "south": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 14, 11, 2 ], "to": [ 14, 16, 14 ], "faces": { - "west": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "west": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 2, 11, 14 ], "to": [ 14, 16, 14 ], "faces": { - "north": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "north": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 2, 11, 2 ], "to": [ 2, 16, 14 ], "faces": { - "east": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "east": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { diff --git a/common/src/main/resources/assets/minecraft/models/block/hopper_side.json b/common/src/main/resources/assets/minecraft/models/block/hopper_side.json index 10667bdac5..21973342ad 100644 --- a/common/src/main/resources/assets/minecraft/models/block/hopper_side.json +++ b/common/src/main/resources/assets/minecraft/models/block/hopper_side.json @@ -1,9 +1,9 @@ { "ambientocclusion": false, "textures": { - "rim": "block/hopper_top", + "top": "block/hopper_top", "particle": "block/hopper_outside", - "outside": "block/hopper_outside", + "side": "block/hopper_outside", "inside": "block/hopper_inside" }, "elements": [ @@ -11,10 +11,10 @@ "from": [ 6, 4, 0 ], "to": [ 10, 8, 4 ], "faces": { - "north": { "uv": [ 6, 8, 10, 12 ], "texture": "#outside", "cullface": "north" }, - "east": { "uv": [ 12, 8, 16, 12 ], "texture": "#outside" }, - "west": { "uv": [ 0, 8, 4, 12 ], "texture": "#outside" }, - "up": { "uv": [ 6, 0, 10, 4 ], "texture": "#outside" }, + "north": { "uv": [ 6, 8, 10, 12 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 12, 8, 16, 12 ], "texture": "#side" }, + "west": { "uv": [ 0, 8, 4, 12 ], "texture": "#side" }, + "up": { "uv": [ 6, 0, 10, 4 ], "texture": "#side" }, "down": { "uv": [ 6, 12, 10, 16 ], "texture": "#inside" } } }, @@ -22,10 +22,10 @@ "from": [ 4, 4, 4 ], "to": [ 12, 10, 12 ], "faces": { - "north": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "east": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "south": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, - "west": { "uv": [ 4, 6, 12, 12 ], "texture": "#outside" }, + "north": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "east": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "south": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, + "west": { "uv": [ 4, 6, 12, 12 ], "texture": "#side" }, "down": { "uv": [ 4, 4, 12, 12 ], "texture": "#inside" } } }, @@ -33,10 +33,10 @@ "from": [ 0, 10, 0 ], "to": [ 16, 16, 16 ], "faces": { - "north": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "north" }, - "east": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "east" }, - "south": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "south" }, - "west": { "uv": [ 0, 0, 16, 6 ], "texture": "#outside", "cullface": "west" }, + "north": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 6 ], "texture": "#side", "cullface": "west" }, "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } } }, @@ -44,56 +44,56 @@ "from": [ 0, 16, 0 ], "to": [ 14, 16, 2 ], "faces": { - "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 14, 16, 0 ], "to": [ 16, 16, 14 ], "faces": { - "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 16, 14 ], "to": [ 16, 16, 16 ], "faces": { - "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 0, 16, 2 ], "to": [ 2, 16, 16 ], "faces": { - "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#rim", "cullface": "up" } + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } } }, { "from": [ 2, 11, 2 ], "to": [ 14, 16, 2 ], "faces": { - "south": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "south": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 14, 11, 2 ], "to": [ 14, 16, 14 ], "faces": { - "west": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "west": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 2, 11, 14 ], "to": [ 14, 16, 14 ], "faces": { - "north": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "north": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { "from": [ 2, 11, 2 ], "to": [ 2, 16, 14 ], "faces": { - "east": { "uv": [ 2, 0, 14, 5 ], "texture": "#outside", "cullface": "up" } + "east": { "uv": [ 2, 0, 14, 5 ], "texture": "#side", "cullface": "up" } } }, { diff --git a/common/src/main/resources/assets/minecraft/models/block/template_cauldron_full.json b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_full.json new file mode 100644 index 0000000000..98f15eb9f9 --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_full.json @@ -0,0 +1,221 @@ +{ + "ambientocclusion": false, + "textures": { + "top": "block/cauldron_top", + "particle": "block/cauldron_side", + "side": "block/cauldron_side", + "inside": "block/cauldron_inner", + "bottom": "block/cauldron_bottom" + }, + "elements": [ + { + "from": [ 0, 3, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "west" }, + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 16, 0 ], + "to": [ 14, 16, 2 ], + "faces": { + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 14, 16, 0 ], + "to": [ 16, 16, 14 ], + "faces": { + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 16, 14 ], + "to": [ 16, 16, 16 ], + "faces": { + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 0, 16, 2 ], + "to": [ 2, 16, 16 ], + "faces": { + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 16, 2 ], + "faces": { + "south": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 14, 4, 2 ], + "to": [ 14, 16, 14 ], + "faces": { + "west": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 14 ], + "to": [ 14, 16, 14 ], + "faces": { + "north": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 2, 16, 14 ], + "faces": { + "east": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 4, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 4, 3, 4 ], + "faces": { + "north": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "north" }, + "west": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 4, 3, 16 ], + "faces": { + "south": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 12, 0, 12 ], + "to": [ 16, 3, 16 ], + "faces": { + "east": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "south" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 3, 4 ], + "faces": { + "north": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "east" } + } + }, + { + "from": [ 14, 0, 2 ], + "to": [ 16, 3, 4 ], + "faces": { + "south": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "west": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 14, 12, 16, 14 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 2, 0, 0 ], + "to": [ 4, 3, 2 ], + "faces": { + "east": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "south": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 2, 14, 4, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 2, 3, 14 ], + "faces": { + "north": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "east": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 0, 2, 2, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 14 ], + "to": [ 14, 3, 16 ], + "faces": { + "north": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "west": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "down": { "uv": [ 12, 0, 14, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 0, 2 ], + "faces": { + "down": { "uv": [ 12, 14, 16, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 2, 0, 4 ], + "faces": { + "down": { "uv": [ 0, 12, 2, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 14 ], + "to": [ 4, 0, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 4, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 0, 16 ], + "faces": { + "down": { "uv": [ 14, 0, 16, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 14, 3, 2 ], + "faces": { + "south": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "west": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 0, 0, 2 ], + "to": [ 2, 3, 4 ], + "faces": { + "east": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "south": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 0, 14 ], + "to": [ 4, 3, 16 ], + "faces": { + "north": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "east": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 3, 14 ], + "faces": { + "north": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" }, + "west": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 15, 2 ], + "to": [ 14, 15, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#content", "tintindex": 0 } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level1.json b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level1.json new file mode 100644 index 0000000000..c04f6ba3ae --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level1.json @@ -0,0 +1,221 @@ +{ + "ambientocclusion": false, + "textures": { + "top": "block/cauldron_top", + "particle": "block/cauldron_side", + "side": "block/cauldron_side", + "inside": "block/cauldron_inner", + "bottom": "block/cauldron_bottom" + }, + "elements": [ + { + "from": [ 0, 3, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "west" }, + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 16, 0 ], + "to": [ 14, 16, 2 ], + "faces": { + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 14, 16, 0 ], + "to": [ 16, 16, 14 ], + "faces": { + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 16, 14 ], + "to": [ 16, 16, 16 ], + "faces": { + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 0, 16, 2 ], + "to": [ 2, 16, 16 ], + "faces": { + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 16, 2 ], + "faces": { + "south": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 14, 4, 2 ], + "to": [ 14, 16, 14 ], + "faces": { + "west": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 14 ], + "to": [ 14, 16, 14 ], + "faces": { + "north": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 2, 16, 14 ], + "faces": { + "east": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 4, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 4, 3, 4 ], + "faces": { + "north": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "north" }, + "west": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 4, 3, 16 ], + "faces": { + "south": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 12, 0, 12 ], + "to": [ 16, 3, 16 ], + "faces": { + "east": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "south" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 3, 4 ], + "faces": { + "north": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "east" } + } + }, + { + "from": [ 14, 0, 2 ], + "to": [ 16, 3, 4 ], + "faces": { + "south": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "west": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 14, 12, 16, 14 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 2, 0, 0 ], + "to": [ 4, 3, 2 ], + "faces": { + "east": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "south": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 2, 14, 4, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 2, 3, 14 ], + "faces": { + "north": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "east": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 0, 2, 2, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 14 ], + "to": [ 14, 3, 16 ], + "faces": { + "north": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "west": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "down": { "uv": [ 12, 0, 14, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 0, 2 ], + "faces": { + "down": { "uv": [ 12, 14, 16, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 2, 0, 4 ], + "faces": { + "down": { "uv": [ 0, 12, 2, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 14 ], + "to": [ 4, 0, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 4, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 0, 16 ], + "faces": { + "down": { "uv": [ 14, 0, 16, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 14, 3, 2 ], + "faces": { + "south": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "west": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 0, 0, 2 ], + "to": [ 2, 3, 4 ], + "faces": { + "east": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "south": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 0, 14 ], + "to": [ 4, 3, 16 ], + "faces": { + "north": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "east": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 3, 14 ], + "faces": { + "north": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" }, + "west": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 9, 2 ], + "to": [ 14, 9, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#content", "tintindex": 0 } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level2.json b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level2.json new file mode 100644 index 0000000000..ef4fce285d --- /dev/null +++ b/common/src/main/resources/assets/minecraft/models/block/template_cauldron_level2.json @@ -0,0 +1,221 @@ +{ + "ambientocclusion": false, + "textures": { + "top": "block/cauldron_top", + "particle": "block/cauldron_side", + "side": "block/cauldron_side", + "inside": "block/cauldron_inner", + "bottom": "block/cauldron_bottom" + }, + "elements": [ + { + "from": [ 0, 3, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 13 ], "texture": "#side", "cullface": "west" }, + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 16, 0 ], + "to": [ 14, 16, 2 ], + "faces": { + "up": { "uv": [ 0, 0, 14, 2 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 14, 16, 0 ], + "to": [ 16, 16, 14 ], + "faces": { + "up": { "uv": [ 14, 0, 16, 14 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 16, 14 ], + "to": [ 16, 16, 16 ], + "faces": { + "up": { "uv": [ 2, 14, 16, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 0, 16, 2 ], + "to": [ 2, 16, 16 ], + "faces": { + "up": { "uv": [ 0, 2, 2, 16 ], "texture": "#top", "cullface": "up" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 16, 2 ], + "faces": { + "south": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 14, 4, 2 ], + "to": [ 14, 16, 14 ], + "faces": { + "west": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 14 ], + "to": [ 14, 16, 14 ], + "faces": { + "north": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 2, 16, 14 ], + "faces": { + "east": { "uv": [ 2, 0, 14, 12 ], "texture": "#side" } + } + }, + { + "from": [ 2, 4, 2 ], + "to": [ 14, 4, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#inside" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 4, 3, 4 ], + "faces": { + "north": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "north" }, + "west": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 4, 3, 16 ], + "faces": { + "south": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "south" }, + "west": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "west" } + } + }, + { + "from": [ 12, 0, 12 ], + "to": [ 16, 3, 16 ], + "faces": { + "east": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "east" }, + "south": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "south" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 3, 4 ], + "faces": { + "north": { "uv": [ 0, 13, 4, 16 ], "texture": "#side", "cullface": "north" }, + "east": { "uv": [ 12, 13, 16, 16 ], "texture": "#side", "cullface": "east" } + } + }, + { + "from": [ 14, 0, 2 ], + "to": [ 16, 3, 4 ], + "faces": { + "south": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "west": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 14, 12, 16, 14 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 2, 0, 0 ], + "to": [ 4, 3, 2 ], + "faces": { + "east": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "south": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 2, 14, 4, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 12 ], + "to": [ 2, 3, 14 ], + "faces": { + "north": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "east": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "down": { "uv": [ 0, 2, 2, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 14 ], + "to": [ 14, 3, 16 ], + "faces": { + "north": { "uv": [ 2, 13, 4, 16 ], "texture": "#side" }, + "west": { "uv": [ 14, 13, 16, 16 ], "texture": "#side" }, + "down": { "uv": [ 12, 0, 14, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 16, 0, 2 ], + "faces": { + "down": { "uv": [ 12, 14, 16, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 0 ], + "to": [ 2, 0, 4 ], + "faces": { + "down": { "uv": [ 0, 12, 2, 16 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 0, 0, 14 ], + "to": [ 4, 0, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 4, 2 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 0, 16 ], + "faces": { + "down": { "uv": [ 14, 0, 16, 4 ], "texture": "#bottom", "cullface": "down" } + } + }, + { + "from": [ 12, 0, 0 ], + "to": [ 14, 3, 2 ], + "faces": { + "south": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "west": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 0, 0, 2 ], + "to": [ 2, 3, 4 ], + "faces": { + "east": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "south": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 0, 14 ], + "to": [ 4, 3, 16 ], + "faces": { + "north": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" }, + "east": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" } + } + }, + { + "from": [ 14, 0, 12 ], + "to": [ 16, 3, 14 ], + "faces": { + "north": { "uv": [ 0, 13, 2, 16 ], "texture": "#side" }, + "west": { "uv": [ 12, 13, 14, 16 ], "texture": "#side" } + } + }, + { + "from": [ 2, 12, 2 ], + "to": [ 14, 12, 14 ], + "faces": { + "up": { "uv": [ 2, 2, 14, 14 ], "texture": "#content", "tintindex": 0 } + } + } + ] +} diff --git a/common/src/main/resources/assets/minecraft/textures/block/acacia_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/acacia_leaves.png index f0a470c38c..55c7deb58b 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/acacia_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/acacia_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/azalea_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/azalea_leaves.png index 5ad7641bd1..ca910846a8 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/azalea_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/azalea_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/birch_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/birch_leaves.png index a1cbce0273..24ca726fbd 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/birch_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/birch_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/cherry_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/cherry_leaves.png index 0ec0eb21f6..d904d41544 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/cherry_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/cherry_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/dark_oak_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/dark_oak_leaves.png index d249b77554..69813be169 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/dark_oak_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/dark_oak_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/flowering_azalea_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/flowering_azalea_leaves.png index a82c5c1b7b..0f2a75e97a 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/flowering_azalea_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/flowering_azalea_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/jungle_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/jungle_leaves.png index e2a930970d..5f18953b13 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/jungle_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/jungle_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/mangrove_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/mangrove_leaves.png index 8891092c05..1fe0289ad9 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/mangrove_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/mangrove_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/oak_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/oak_leaves.png index 4bb353cc01..66980b98a8 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/oak_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/oak_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/pale_oak_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/pale_oak_leaves.png new file mode 100644 index 0000000000..7a0f81ba2c Binary files /dev/null and b/common/src/main/resources/assets/minecraft/textures/block/pale_oak_leaves.png differ diff --git a/common/src/main/resources/assets/minecraft/textures/block/spruce_leaves.png b/common/src/main/resources/assets/minecraft/textures/block/spruce_leaves.png index 8bd6cdba41..d37a65140f 100644 Binary files a/common/src/main/resources/assets/minecraft/textures/block/spruce_leaves.png and b/common/src/main/resources/assets/minecraft/textures/block/spruce_leaves.png differ diff --git a/common/src/main/resources/assets/sodium/lang/en_us.json b/common/src/main/resources/assets/sodium/lang/en_us.json index d1d1e6b6a7..3694f6b6b4 100644 --- a/common/src/main/resources/assets/sodium/lang/en_us.json +++ b/common/src/main/resources/assets/sodium/lang/en_us.json @@ -4,6 +4,7 @@ "sodium.option_impact.high": "High", "sodium.option_impact.extreme": "Extreme", "sodium.option_impact.varies": "Varies", + "sodium.options.pages.general" : "General", "sodium.options.pages.quality": "Quality", "sodium.options.pages.performance": "Performance", "sodium.options.pages.advanced": "Advanced", @@ -12,7 +13,7 @@ "sodium.options.brightness.tooltip": "Controls the minimum brightness in the world. When increased, darker areas of the world will appear brighter. This does not affect the brightness of already well-lit areas.", "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.fullscreen_resolution.tooltip": "The monitor resolution and refresh rate to be used when in fullscreen mode. Changing this option may interfere with other applications and cause a delay when switching between applications.\n\nThis is only supported on the Windows operating system.", + "sodium.options.fullscreen_resolution.tooltip": "The monitor resolution and refresh rate to be used when in fullscreen mode. Changing this option may interfere with other applications and cause a delay when switching between applications.\n\nThis is only supported on the Windows or macOS operating systems.", "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 system load when multi-tasking. If VSync 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 may benefit from disabling this.", @@ -21,12 +22,17 @@ "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": "The quality level used for rendering clouds in the sky. Even though the options are labeled \"Fast\" and \"Fancy\", the effect on performance is negligible.", "sodium.options.weather_quality.tooltip": "Controls the distance that weather effects, such as rain and snow, will be rendered.", + "sodium.options.weather_quality.fast": "Fast", + "sodium.options.weather_quality.fancy": "Fancy", "sodium.options.leaves_quality.name": "Leaves", + "sodium.options.leaves_quality.fast": "Fast", + "sodium.options.leaves_quality.fancy": "Fancy", "sodium.options.leaves_quality.tooltip": "Controls whether leaves will be rendered as transparent (fancy) or opaque (fast).", "sodium.options.particle_quality.tooltip": "Controls the maximum number of particles which can be present on screen at any one time.", + "sodium.options.chunk_fade_time.value" : "%s Seconds", "sodium.options.smooth_lighting.tooltip": "Enables the smooth lighting and shading of blocks in the world. This can very slightly increase the amount of time it takes to load or update a chunk, but it doesn't affect frame rates.", - "sodium.options.biome_blend.value": "%s block(s)", - "sodium.options.biome_blend.tooltip": "The distance (in blocks) which biome colors are smoothly blended across. Using higher values will greatly increase the amount of time it takes to load or update chunks, for diminishing improvements in quality.", + "sodium.options.biome_blend.value": "%sx%s Blocks", + "sodium.options.biome_blend.tooltip": "The area (in blocks) where biome colors are smoothly blended across. Default is 5x5. Higher values will greatly increase the amount of time it takes to load or update chunks, for diminishing improvements in quality.", "sodium.options.entity_distance.tooltip": "The render distance multiplier used by entity rendering. Smaller values decrease, and larger values increase, the maximum distance at which entities will be rendered.", "sodium.options.entity_shadows.tooltip": "If enabled, basic shadows will be rendered beneath mobs and other entities.", "sodium.options.vignette.name": "Vignette", @@ -47,11 +53,23 @@ "sodium.options.use_persistent_mapping.name": "Use Persistent Mapping", "sodium.options.use_persistent_mapping.tooltip": "For debugging only. If enabled, persistent memory mappings will be used for the staging buffer so that unnecessary memory copies can be avoided. Disabling this can be useful for narrowing down the cause of graphical corruption.\n\nRequires OpenGL 4.4 or ARB_buffer_storage.", "sodium.options.chunk_update_threads.name": "Chunk Update Threads", + "sodium.options.chunk_update_threads.value": "%s Threads", + "sodium.options.default": "Default", "sodium.options.chunk_update_threads.tooltip": "Specifies the number of threads to use for chunk building and sorting. Using more threads can speed up chunk loading and update speed, but may negatively impact frame times. The default value is usually good enough for all situations.", - "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 where blocks take a while to appear or disappear.", - "sodium.options.sort_behavior.name": "Translucency Sorting", - "sodium.options.sort_behavior.tooltip": "Enables translucency sorting. This avoids glitches in translucent blocks like water and glass when enabled and attempts to correctly present them even when the camera is in motion. This has a small performance impact on chunk loading and update speeds, but is usually not noticeable in frame rates.", + "sodium.options.defer_chunk_updates.name": "Chunk Updates", + "sodium.options.defer_chunk_updates.tooltip": "If set to \"Deferred\", rendering will never wait for nearby 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 where blocks take a while to appear or disappear. \"Immediate\" eliminates visual lag by blocking the frame until chunk updates are complete while \"Soon\" allows at most one frame of visual lag.", + "sodium.options.defer_chunk_updates.always": "Deferred", + "sodium.options.defer_chunk_updates.one_frame": "Soon", + "sodium.options.defer_chunk_updates.zero_frames": "Immediate", + "sodium.options.quad_splitting.name": "Block Transparency Limits", + "sodium.options.quad_splitting.tooltip": "Determines what approach is used to make translucent blocks (not entities or items) look correct even when they intersect or have unusual shapes, such as waterlogged stained glass panes. \"Basic\" disables this feature and such blocks may render incorrectly. \"Safe\" limits the amount of newly generated geometry to prevent instability in extreme cases, while \"Unlimited\" imposes no limit.", + "sodium.options.quad_splitting.off": "Basic", + "sodium.options.quad_splitting.safe": "Safe", + "sodium.options.quad_splitting.unlimited": "Unlimited", + "sodium.options.closest_point_entity_sort.name" : "Entity Sorting", + "sodium.options.closest_point_entity_sort.tooltip" : "Controls how the translucent geometry of entities is sorted. \"Enhanced\" sorts by the closest point to the camera, which improves rendering accuracy for translucent quads with different sizes, at a small CPU cost. \"Default\" uses the standard sorting.", + "sodium.options.closest_point_entity_sort.default" : "Default", + "sodium.options.closest_point_entity_sort.enhanced" : "Enhanced", "sodium.options.use_no_error_context.name": "Use No Error Context", "sodium.options.use_no_error_context.tooltip": "When enabled, the OpenGL context will be created with error checking disabled. This slightly improves rendering performance, but it can make debugging sudden unexplained crashes much harder.", "sodium.options.buttons.undo": "Undo", @@ -59,10 +77,15 @@ "sodium.options.buttons.donate": "Buy us a coffee!", "sodium.console.game_restart": "The game must be restarted to apply one or more video settings!", "sodium.console.broken_nvidia_driver": "Your NVIDIA graphics drivers are out of date!\n * This will cause severe performance issues and crashes when Sodium is installed.\n * Please update your graphics drivers to the latest version (version 536.23 or newer.)", - "sodium.console.pojav_launcher": "PojavLauncher is not supported when using Sodium.\n * You are very likely to run into extreme performance issues, graphical bugs, and crashes.\n * You will be on your own if you decide to continue -- we will not help you with any bugs or crashes!", "sodium.console.core_shaders_error": "The following resource packs are incompatible with Sodium:", "sodium.console.core_shaders_warn": "The following resource packs may be incompatible with Sodium:", "sodium.console.core_shaders_info": "Check the game log for detailed information.", "sodium.console.config_not_loaded": "The configuration file for Sodium has been corrupted, or is currently unreadable. Some options have been temporarily reset to their defaults. Please open the Video Settings screen to resolve this problem.", - "sodium.console.config_file_was_reset": "The config file has been reset to known-good defaults." + "sodium.console.corrupt_config.console.config_file_was_reset": "The config file has been reset to known-good defaults.", + "sodium.console.corrupt_config.console.title": "Sodium failed to load the configuration file", + "sodium.console.corrupt_config.message.title": "Could not load the configuration file", + "sodium.console.corrupt_config.message.body": "A problem occurred while trying to load the configuration file. This\ncan happen when the file has been corrupted on disk, or when trying\nto manually edit the file by hand.\n\nIf you continue, the configuration file will be reset back to known-good\ndefaults, and you will lose any changes that have since been made to your\nVideo Settings.\n\nMore information about the error can be found in the log file.", + "sodium.options.search": "Search", + "sodium.options.search.hint": "Search...", + "sodium.options.open_external_page_button": "Open" } diff --git a/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.fsh b/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.fsh index 314e696962..49de52fc33 100644 --- a/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.fsh +++ b/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.fsh @@ -1,13 +1,17 @@ #version 330 core +#ifndef MAX_TEXTURE_LOD_BIAS +#error "MAX_TEXTURE_LOD_BIAS constant not specified" +#endif + #import +#import in vec4 v_Color; // The interpolated vertex color in vec2 v_TexCoord; // The interpolated block texture coordinates in float v_FragDistance; // The fragment's distance from the camera -in float v_MaterialMipBias; -in float v_MaterialAlphaCutoff; +flat in uint v_Material; uniform sampler2D u_BlockTex; // The block texture @@ -18,16 +22,16 @@ uniform float u_FogEnd; // The ending position of the shader fog out vec4 fragColor; // The output fragment for the color framebuffer void main() { - vec4 diffuseColor = texture(u_BlockTex, v_TexCoord, v_MaterialMipBias); + float lodBias = _material_use_mips(v_Material) ? 0.0 : float(-MAX_TEXTURE_LOD_BIAS); - // Apply per-vertex color - diffuseColor *= v_Color; + vec4 color = texture(u_BlockTex, v_TexCoord, lodBias); + color *= v_Color; // Apply per-vertex color modulator #ifdef USE_FRAGMENT_DISCARD - if (diffuseColor.a < v_MaterialAlphaCutoff) { + if (color.a < _material_alpha_cutoff(v_Material)) { discard; } #endif - fragColor = _linearFog(diffuseColor, v_FragDistance, u_FogColor, u_FogStart, u_FogEnd); + fragColor = _linearFog(color, v_FragDistance, u_FogColor, u_FogStart, u_FogEnd); } \ No newline at end of file diff --git a/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh b/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh index e281de1eae..4ba93a38e5 100644 --- a/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh +++ b/common/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh @@ -3,15 +3,11 @@ #import #import #import -#import out vec4 v_Color; out vec2 v_TexCoord; -out float v_MaterialMipBias; -#ifdef USE_FRAGMENT_DISCARD -out float v_MaterialAlphaCutoff; -#endif +flat out uint v_Material; #ifdef USE_FOG out float v_FragDistance; @@ -19,6 +15,7 @@ out float v_FragDistance; uniform int u_FogShape; uniform vec3 u_RegionOffset; +uniform vec2 u_TexCoordShrink; uniform sampler2D u_LightTex; // The light map texture sampler @@ -47,10 +44,7 @@ void main() { // Add the light color to the vertex color, and pass the texture coordinates to the fragment shader v_Color = _vert_color * texture(u_LightTex, _vert_tex_light_coord); - v_TexCoord = _vert_tex_diffuse_coord; + v_TexCoord = (_vert_tex_diffuse_coord_bias * u_TexCoordShrink) + _vert_tex_diffuse_coord; // FMA for precision - v_MaterialMipBias = _material_mip_bias(_material_params); -#ifdef USE_FRAGMENT_DISCARD - v_MaterialAlphaCutoff = _material_alpha_cutoff(_material_params); -#endif + v_Material = _material_params; } diff --git a/common/src/main/resources/assets/sodium/shaders/include/chunk_material.glsl b/common/src/main/resources/assets/sodium/shaders/include/chunk_material.glsl index bd73ad3976..11e8e0112b 100644 --- a/common/src/main/resources/assets/sodium/shaders/include/chunk_material.glsl +++ b/common/src/main/resources/assets/sodium/shaders/include/chunk_material.glsl @@ -3,8 +3,8 @@ const uint MATERIAL_ALPHA_CUTOFF_OFFSET = 1u; const float[4] ALPHA_CUTOFF = float[4](0.0, 0.1, 0.1, 1.0); -float _material_mip_bias(uint material) { - return ((material >> MATERIAL_USE_MIP_OFFSET) & 1u) != 0u ? 0.0 : -4.0; +bool _material_use_mips(uint material) { + return ((material >> MATERIAL_USE_MIP_OFFSET) & 1u) != 0u; } float _material_alpha_cutoff(uint material) { diff --git a/common/src/main/resources/assets/sodium/shaders/include/chunk_vertex.glsl b/common/src/main/resources/assets/sodium/shaders/include/chunk_vertex.glsl index 3ce9949258..e392ffa901 100644 --- a/common/src/main/resources/assets/sodium/shaders/include/chunk_vertex.glsl +++ b/common/src/main/resources/assets/sodium/shaders/include/chunk_vertex.glsl @@ -3,6 +3,7 @@ vec3 _vert_position; // The block texture coordinate of the vertex vec2 _vert_tex_diffuse_coord; +vec2 _vert_tex_diffuse_coord_bias; // The light texture coordinate of the vertex vec2 _vert_tex_light_coord; @@ -28,10 +29,6 @@ const uint TEXTURE_MAX_VALUE = TEXTURE_MAX_COORD - 1u; const float VERTEX_SCALE = 32.0 / float(POSITION_MAX_COORD); const float VERTEX_OFFSET = -8.0; -// The amount of inset the texture coordinates from the edges of the texture, to avoid texture bleeding -const float TEXTURE_FUZZ_AMOUNT = 1.0 / 64.0; -const float TEXTURE_GROW_FACTOR = (1.0 - TEXTURE_FUZZ_AMOUNT) / TEXTURE_MAX_COORD; - in uvec2 a_Position; in vec4 a_Color; in uvec2 a_TexCoord; @@ -49,13 +46,14 @@ vec2 _get_texcoord() { } vec2 _get_texcoord_bias() { - return mix(vec2(-TEXTURE_GROW_FACTOR), vec2(TEXTURE_GROW_FACTOR), bvec2(a_TexCoord >> TEXTURE_BITS)); + return mix(vec2(-1.0), vec2(1.0), bvec2(a_TexCoord >> TEXTURE_BITS)); } void _vert_init() { _vert_position = (_deinterleave_u20x3(a_Position) * VERTEX_SCALE) + VERTEX_OFFSET; _vert_color = a_Color; - _vert_tex_diffuse_coord = _get_texcoord() + _get_texcoord_bias(); + _vert_tex_diffuse_coord = _get_texcoord(); + _vert_tex_diffuse_coord_bias = _get_texcoord_bias(); _vert_tex_light_coord = vec2(a_LightAndData.xy) / vec2(256.0); diff --git a/common/src/main/resources/assets/sodium/textures/gui/arrows.png b/common/src/main/resources/assets/sodium/textures/gui/arrows.png deleted file mode 100644 index 01161492cb..0000000000 Binary files a/common/src/main/resources/assets/sodium/textures/gui/arrows.png and /dev/null differ diff --git a/common/src/main/resources/assets/sodium/textures/gui/coffee_cup.png b/common/src/main/resources/assets/sodium/textures/gui/coffee_cup.png new file mode 100644 index 0000000000..872b41ff20 Binary files /dev/null and b/common/src/main/resources/assets/sodium/textures/gui/coffee_cup.png differ diff --git a/common/src/main/resources/assets/sodium/textures/gui/config-icon.png b/common/src/main/resources/assets/sodium/textures/gui/config-icon.png new file mode 100644 index 0000000000..882e4c775b Binary files /dev/null and b/common/src/main/resources/assets/sodium/textures/gui/config-icon.png differ diff --git a/common/src/main/resources/assets/sodium/textures/gui/reset_button.png b/common/src/main/resources/assets/sodium/textures/gui/reset_button.png new file mode 100644 index 0000000000..88e548a630 Binary files /dev/null and b/common/src/main/resources/assets/sodium/textures/gui/reset_button.png differ diff --git a/common/src/main/resources/assets/sodium/textures/gui/tooltip_arrows.png b/common/src/main/resources/assets/sodium/textures/gui/tooltip_arrows.png new file mode 100644 index 0000000000..1a9f2ef4ea Binary files /dev/null and b/common/src/main/resources/assets/sodium/textures/gui/tooltip_arrows.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/acacia_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/acacia_leaves.png index 8a7c3e9818..be69d28497 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/acacia_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/acacia_leaves.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/birch_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/birch_leaves.png index 8a7c3e9818..be69d28497 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/birch_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/birch_leaves.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/dark_oak_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/dark_oak_leaves.png index 8a7c3e9818..be69d28497 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/dark_oak_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/dark_oak_leaves.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/jungle_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/jungle_leaves.png index 51a18016e8..745f99868f 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/jungle_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/jungle_leaves.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/oak_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/oak_leaves.png index 8a7c3e9818..be69d28497 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/oak_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/oak_leaves.png differ diff --git a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/spruce_leaves.png b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/spruce_leaves.png index 713b2dcd15..8c598d891e 100644 Binary files a/common/src/main/resources/programmer_art/assets/minecraft/textures/block/spruce_leaves.png and b/common/src/main/resources/programmer_art/assets/minecraft/textures/block/spruce_leaves.png differ diff --git a/common/src/main/resources/sodium-common.mixins.json b/common/src/main/resources/sodium-common.mixins.json index a41962e84e..59a2423df1 100644 --- a/common/src/main/resources/sodium-common.mixins.json +++ b/common/src/main/resources/sodium-common.mixins.json @@ -1,21 +1,22 @@ { "package": "net.caffeinemc.mods.sodium.mixin", "required": true, - "compatibilityLevel": "JAVA_17", + "compatibilityLevel": "JAVA_21", "plugin": "net.caffeinemc.mods.sodium.mixin.SodiumMixinPlugin", "injectors": { "defaultRequire": 1 }, "overwrites": { - "conformVisibility": true + "conformVisibility": true, + "requireAnnotations": true }, "client": [ "core.MinecraftMixin", "core.WindowMixin", "core.gui.LevelLoadStatusManagerMixin", + "core.model.TextureAtlasSpriteMixin", "core.model.colors.BlockColorsMixin", "core.model.colors.ItemColorsMixin", - "core.model.TextureAtlasSpriteMixin", "core.render.BlockEntityTypeMixin", "core.render.TextureAtlasMixin", "core.render.VertexFormatMixin", @@ -27,22 +28,23 @@ "core.render.immediate.consumer.VertexMultiConsumerMixin$DoubleMixin", "core.render.immediate.consumer.VertexMultiConsumerMixin$MultipleMixin", "core.render.texture.TextureAtlasAccessor", - "core.render.world.RenderBuffersMixin", "core.render.world.LevelRendererMixin", + "core.render.world.RenderBuffersMixin", "core.world.biome.ClientLevelMixin", - "core.world.chunk.ZeroBitStorageMixin", - "core.world.chunk.SimpleBitStorageMixin", "core.world.chunk.PalettedContainerMixin", + "core.world.chunk.SimpleBitStorageMixin", + "core.world.chunk.ZeroBitStorageMixin", "core.world.map.ClientChunkCacheMixin", - "core.world.map.ClientPacketListenerMixin", "core.world.map.ClientLevelMixin", + "core.world.map.ClientPacketListenerMixin", + "debug.checks.threading.MixinRenderSystem", "features.gui.hooks.console.GameRendererMixin", "features.gui.hooks.debug.DebugScreenOverlayMixin", "features.gui.hooks.settings.OptionsScreenMixin", "features.gui.screen.LevelLoadingScreenMixin", "features.options.overlays.GuiMixin", - "features.options.render_layers.LeavesBlockMixin", "features.options.render_layers.ItemBlockRenderTypesMixin", + "features.options.render_layers.LeavesBlockMixin", "features.options.weather.LevelRendererMixin", "features.render.compositing.RenderTargetMixin", "features.render.entity.CubeMixin", @@ -50,43 +52,48 @@ "features.render.entity.cull.EntityRendererMixin", "features.render.entity.shadows.EntityRenderDispatcherMixin", "features.render.frapi.BakedModelMixin", - "features.render.frapi.ModelBlockRendererMixin", - "features.render.frapi.ItemRendererMixin", "features.render.frapi.ItemRendererAccessor", + "features.render.frapi.ItemRendererMixin", + "features.render.frapi.ModelBlockRendererMixin", "features.render.gui.font.BakedGlyphMixin", "features.render.gui.outlines.LevelRendererMixin", "features.render.immediate.DirectionMixin", - "features.render.immediate.buffer_builder.intrinsics.BufferBuilderMixin", - "features.render.immediate.buffer_builder.sorting.MeshDataMixin", + "features.render.immediate.buffer_builder.intrinsics.BufferBuilderAccessor", + "features.render.immediate.buffer_builder.intrinsics.VertexConsumerMixin", + "features.render.immediate.buffer_builder.sorting.MeshDataAccessor", + "features.render.immediate.buffer_builder.sorting.MultiBufferSourceMixin", "features.render.immediate.buffer_builder.sorting.VertexSortingMixin", "features.render.immediate.matrix_stack.PoseStackMixin", "features.render.immediate.matrix_stack.VertexConsumerMixin", "features.render.model.ItemBlockRenderTypesMixin", "features.render.model.item.ItemRendererMixin", "features.render.particle.SingleQuadParticleMixin", + "features.render.viewport.GlStateManagerMixin", "features.render.world.clouds.LevelRendererMixin", - "features.render.world.sky.FogRendererMixin", "features.render.world.sky.ClientLevelMixin", + "features.render.world.sky.FogRendererMixin", "features.render.world.sky.LevelRendererMixin", "features.shader.uniform.ShaderInstanceMixin", "features.textures.NativeImageAccessor", "features.textures.SpriteContentsInvoker", - "features.textures.animations.tracking.ModelBlockRendererMixin", - "features.textures.animations.tracking.GuiGraphicsMixin", - "features.textures.animations.tracking.TextureAtlasMixin", - "features.textures.animations.tracking.TextureSheetParticleMixin", "features.textures.animations.tracking.AnimatedTextureAccessor", + "features.textures.animations.tracking.GuiGraphicsMixin", + "features.textures.animations.tracking.ModelBlockRendererMixin", "features.textures.animations.tracking.SpriteContentsFrameInfoAccessor", - "features.textures.animations.tracking.SpriteContentsTickerMixin", "features.textures.animations.tracking.SpriteContentsMixin", + "features.textures.animations.tracking.SpriteContentsTickerMixin", + "features.textures.animations.tracking.TextureAtlasMixin", + "features.textures.animations.tracking.TextureAtlasSpriteMixin", + "features.textures.animations.tracking.TextureSheetParticleMixin", "features.textures.animations.upload.SpriteContentsAccessor", "features.textures.animations.upload.SpriteContentsAnimatedTextureAccessor", "features.textures.animations.upload.SpriteContentsFrameInfoAccessor", - "features.textures.animations.upload.SpriteContentsTickerAccessor", "features.textures.animations.upload.SpriteContentsInterpolationMixin", + "features.textures.animations.upload.SpriteContentsTickerAccessor", "features.textures.mipmaps.MipmapGeneratorMixin", "features.textures.mipmaps.SpriteContentsMixin", "features.textures.scan.SpriteContentsMixin", + "features.textures.scan.TextureAtlasSpriteMixin", "workarounds.context_creation.WindowMixin", "workarounds.event_loop.RenderSystemMixin" ] diff --git a/common/src/main/resources/sodium-icon.png b/common/src/main/resources/sodium-icon.png index b47ebbd930..2a9874a9e8 100644 Binary files a/common/src/main/resources/sodium-icon.png and b/common/src/main/resources/sodium-icon.png differ diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java b/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java deleted file mode 100644 index c4ef0b2a3e..0000000000 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.caffeinemc.mods.sodium.client.compatibility.checks; - -/** - * "Checks" are used to determine whether the environment we are running within is actually reasonable. Most often, - * failing checks will crash the game and prompt the user for intervention. - */ -class BugChecks { - public static final boolean ISSUE_899 = configureCheck("issue899", true); - public static final boolean ISSUE_1486 = configureCheck("issue1486", true); - public static final boolean ISSUE_2048 = configureCheck("issue2048", true); - public static final boolean ISSUE_2561 = configureCheck("issue2561", true); - public static final boolean ISSUE_2637 = configureCheck("issue2637", true); - - private static boolean configureCheck(String name, boolean defaultValue) { - var propertyValue = System.getProperty(getPropertyKey(name), null); - - if (propertyValue == null) { - return defaultValue; - } - - return Boolean.parseBoolean(propertyValue); - } - - private static String getPropertyKey(String name) { - return "sodium.checks." + name; - } -} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java b/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java deleted file mode 100644 index cd41ca489c..0000000000 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java +++ /dev/null @@ -1,173 +0,0 @@ -package net.caffeinemc.mods.sodium.client.compatibility.checks; - -import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor; -import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaDriverVersion; -import net.caffeinemc.mods.sodium.client.platform.MessageBox; -import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion; -import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.Version; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; - -/** - * Performs OpenGL driver validation before the game creates an OpenGL context. This runs during the earliest possible - * opportunity at game startup, and uses a custom hardware prober to search for problematic drivers. - */ -public class PreLaunchChecks { - private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-EarlyDriverScanner"); - - // This string should be determined at compile time, so it can be checked against the runtime version. - private static final String REQUIRED_LWJGL_VERSION = Version.VERSION_MAJOR + "." + Version.VERSION_MINOR + "." + Version.VERSION_REVISION; - - private static final String normalMessage = "You must change the LWJGL version in your launcher to continue. This is usually controlled by the settings for a profile or instance in your launcher."; - - private static final String prismMessage = "It appears you are using Prism Launcher to start the game. You can likely fix this problem by opening your instance settings and navigating to the Version section in the sidebar."; - - public static void beforeLWJGLInit() { - if (BugChecks.ISSUE_2561) { - if (!Version.getVersion().startsWith(REQUIRED_LWJGL_VERSION)) { - String message = normalMessage; - - if (System.getProperty("minecraft.launcher.brand", "unknown").equalsIgnoreCase("PrismLauncher")) { - message = prismMessage; - } - - showCriticalErrorAndClose("Sodium Renderer - Unsupported LWJGL", - (""" - The game failed to start because the currently active LWJGL version is not \ - compatible. - - Installed version: ###CURRENT_VERSION### - Required version: ###REQUIRED_VERSION### - - """ + message) - .replace("###CURRENT_VERSION###", org.lwjgl.Version.getVersion()) - .replace("###REQUIRED_VERSION###", REQUIRED_LWJGL_VERSION), - "https://github.com/CaffeineMC/sodium/wiki/LWJGL-Compatibility"); - - } - } - } - - public static void onGameInit() { - if (BugChecks.ISSUE_899) { - var installedVersion = findIntelDriverMatchingBug899(); - - if (installedVersion != null) { - showCriticalErrorAndClose("Sodium Renderer - Unsupported Driver", - """ - The game failed to start because the currently installed Intel Graphics Driver is not \ - compatible. - - Installed version: ###CURRENT_DRIVER### - Required version: 10.18.10.5161 (or newer) - - You must update your graphics card driver in order to continue.""" - .replace("###CURRENT_DRIVER###", installedVersion.toString()), - "https://github.com/CaffeineMC/sodium/wiki/Driver-Compatibility#windows-intel-gen7"); - } - } - - if (BugChecks.ISSUE_1486) { - var installedVersion = findNvidiaDriverMatchingBug1486(); - - if (installedVersion != null) { - showCriticalErrorAndClose("Sodium Renderer - Unsupported Driver", - """ - The game failed to start because the currently installed NVIDIA Graphics Driver is not \ - compatible. - - Installed version: ###CURRENT_DRIVER### - Required version: 536.23 (or newer) - - You must update your graphics card driver in order to continue.""" - .replace("###CURRENT_DRIVER###", NvidiaDriverVersion.parse(installedVersion).toString()), - "https://github.com/CaffeineMC/sodium/wiki/Driver-Compatibility#nvidia-gpus"); - - } - } - } - - private static void showCriticalErrorAndClose(String title, String message, String url) { - // Always print the information to the log file first, just in case we can't show the message box. - LOGGER.error(""" - ###ERROR_DESCRIPTION### - - For more information, please see: ###HELP_URL###""" - .replace("###ERROR_DESCRIPTION###", message) - .replace("###HELP_URL###", url == null ? "" : url)); - - // Try to show a graphical message box (if the platform supports it) and shut down the game. - MessageBox.showMessageBox(null, MessageBox.IconType.ERROR, title, message, url); - System.exit(1 /* failure code */); - } - - // https://github.com/CaffeineMC/sodium/issues/899 - private static @Nullable WindowsFileVersion findIntelDriverMatchingBug899() { - if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) { - return null; - } - - for (var adapter : GraphicsAdapterProbe.getAdapters()) { - if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { - @Nullable var driverName = wddmAdapterInfo.getOpenGlIcdName(); - - if (driverName == null) { - continue; - } - - var driverVersion = wddmAdapterInfo.openglIcdVersion(); - - // Intel OpenGL ICD for Generation 7 GPUs - if (driverName.matches("ig7icd(32|64)")) { - // https://www.intel.com/content/www/us/en/support/articles/000005654/graphics.html - // Anything which matches the 15.33 driver scheme (WDDM x.y.10.w) should be checked - // Drivers before build 5161 are assumed to have bugs with synchronization primitives - if (driverVersion.z() == 10 && driverVersion.w() < 5161) { - return driverVersion; - } - } - } - } - - return null; - } - - - // https://github.com/CaffeineMC/sodium/issues/1486 - // The way which NVIDIA tries to detect the Minecraft process could not be circumvented until fairly recently - // So we require that an up-to-date graphics driver is installed so that our workarounds can disable the Threaded - // Optimizations driver hack. - private static @Nullable WindowsFileVersion findNvidiaDriverMatchingBug1486() { - // The Linux driver has two separate branches which have overlapping version numbers, despite also having - // different feature sets. As a result, we can't reliably determine which Linux drivers are broken... - if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) { - return null; - } - - for (var adapter : GraphicsAdapterProbe.getAdapters()) { - if (adapter.vendor() != GraphicsAdapterVendor.NVIDIA) { - continue; - } - - if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) { - var driverVersion = wddmAdapterInfo.openglIcdVersion(); - - if (driverVersion.z() == 15) { // Only match 5XX.XX drivers - // Broken in x.y.15.2647 (526.47) - // Fixed in x.y.15.3623 (536.23) - if (driverVersion.w() >= 2647 && driverVersion.w() < 3623) { - return driverVersion; - } - } - } - } - - return null; - } -} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java b/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java deleted file mode 100644 index 4f39eb9ce0..0000000000 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.caffeinemc.mods.sodium.client.compatibility.environment; - -import org.apache.commons.lang3.SystemUtils; - -public class OsUtils { - - public static OperatingSystem getOs() { - if (SystemUtils.IS_OS_WINDOWS) { - return OperatingSystem.WIN; - } else if (SystemUtils.IS_OS_MAC) { - return OperatingSystem.MAC; - } else if (SystemUtils.IS_OS_LINUX) { - return OperatingSystem.LINUX; - } - - return OperatingSystem.UNKNOWN; - } - - public enum OperatingSystem { - WIN, - MAC, - LINUX, - UNKNOWN - } -} \ No newline at end of file diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java b/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java deleted file mode 100644 index 0026148242..0000000000 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.caffeinemc.mods.sodium.client.compatibility.environment.probe; - -import org.jetbrains.annotations.NotNull; - -public enum GraphicsAdapterVendor { - NVIDIA, - AMD, - INTEL, - UNKNOWN; - - @NotNull - static GraphicsAdapterVendor fromPciVendorId(String vendor) { - if (vendor.contains("0x1002")) { - return AMD; - } else if (vendor.contains("0x10de")) { - return NVIDIA; - } else if (vendor.contains("0x8086")) { - return INTEL; - } - - return UNKNOWN; - } - - public static GraphicsAdapterVendor fromIcdName(String name) { - // Intel Gen 4, 5, 6 - ig4icd - // Intel Gen 7 - ig7icd - // Intel Gen 7.5 - ig75icd - // Intel Gen 8 - ig8icd - // Intel Gen 9, 9.5 - ig9icd - // Intel Gen 11 - ig11icd - // Intel Gen 12 - ig12icd (UHD Graphics, with early drivers) - // igxelpicd (Xe-LP; integrated) - // igxehpicd (Xe-HP; dedicated) - if (name.matches("ig(4|7|75|8|9|11|12|xelp|xehp)icd(32|64)")) { - return INTEL; - } - - if (name.matches("nvoglv(32|64)")) { - return NVIDIA; - } - - if (name.matches("atiglpxx|atig6pxx")) { - return AMD; - } - - return UNKNOWN; - } -} diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java b/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java deleted file mode 100644 index 5f54ec5174..0000000000 --- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia; - -import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils; -import net.caffeinemc.mods.sodium.client.platform.unix.Libc; -import net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32; -import net.caffeinemc.mods.sodium.client.platform.windows.WindowsCommandLine; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class NvidiaWorkarounds { - private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-NvidiaWorkarounds"); - - public static void install() { - LOGGER.warn("Applying workaround: Prevent the NVIDIA OpenGL driver from using broken optimizations (NVIDIA_THREADED_OPTIMIZATIONS)"); - - try { - switch (OsUtils.getOs()) { - case WIN -> { - // The NVIDIA drivers rely on parsing the command line arguments to detect Minecraft. If we destroy those, - // then it shouldn't be able to detect us anymore. - WindowsCommandLine.setCommandLine("net.caffeinemc.sodium"); - - // Ensures that Minecraft will run on the dedicated GPU, since the drivers can no longer detect it - Kernel32.setEnvironmentVariable("SHIM_MCCOMPAT", "0x800000001"); - } - case LINUX -> { - // Unlike Windows, we don't need to hide ourselves from the driver. We can just request that - // it not use threaded optimizations instead. - Libc.setEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", "0"); - } - } - } catch (Throwable t) { - LOGGER.error("Failure while applying workarounds", t); - - LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!"); - LOGGER.error("READ ME! You are very likely going to run into unexplained crashes and severe performance issues!"); - LOGGER.error("READ ME! Please see this issue for more information: https://github.com/CaffeineMC/sodium/issues/1816"); - } - } - - public static void uninstall() { - switch (OsUtils.getOs()) { - case WIN -> { - WindowsCommandLine.resetCommandLine(); - } - case LINUX -> { } - } - } -} diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index d8c96b3940..f8270d88d5 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,34 +1,50 @@ +import net.fabricmc.loom.task.RemapJarTask +import net.fabricmc.loom.task.RemapSourcesJarTask + plugins { id("multiloader-platform") - id("fabric-loom") version ("1.8.9") + id("net.fabricmc.fabric-loom-remap") version ("1.16.1") } base { archivesName = "sodium-fabric" } +val configurationApiModJava: Configuration = configurations.create("apiJava") { + isCanBeResolved = true +} + val configurationCommonModJava: Configuration = configurations.create("commonJava") { isCanBeResolved = true } + +val configurationApiModSources: Configuration = configurations.create("apiSources") { + isCanBeResolved = true +} + val configurationCommonModResources: Configuration = configurations.create("commonResources") { isCanBeResolved = true } dependencies { configurationCommonModJava(project(path = ":common", configuration = "commonMainJava")) - configurationCommonModJava(project(path = ":common", configuration = "commonApiJava")) - configurationCommonModJava(project(path = ":common", configuration = "commonEarlyLaunchJava")) + configurationApiModJava(project(path = ":common", configuration = "commonApiJava")) + configurationCommonModJava(project(path = ":common", configuration = "commonBootJava")) + + configurationApiModSources(project(path = ":common", configuration = "commonApiSources")) configurationCommonModResources(project(path = ":common", configuration = "commonMainResources")) configurationCommonModResources(project(path = ":common", configuration = "commonApiResources")) - configurationCommonModResources(project(path = ":common", configuration = "commonEarlyLaunchResources")) + configurationCommonModResources(project(path = ":common", configuration = "commonBootResources")) } sourceSets.apply { main { compileClasspath += configurationCommonModJava + compileClasspath += configurationApiModJava runtimeClasspath += configurationCommonModJava + runtimeClasspath += configurationApiModJava } } @@ -80,6 +96,38 @@ loom { tasks { jar { from(configurationCommonModJava) + from(configurationApiModJava) + } + + val apiJar = register("apiJar") { + archiveClassifier.set("api-dev") + from(configurationApiModJava) + from(sourceSets.main.get().resources) + destinationDirectory.set(file(project.layout.buildDirectory).resolve("devlibs")) + } + + val apiSourcesJar = register("apiSourcesJar") { + archiveClassifier.set("api-sources-dev") + from(configurationApiModSources) + from(sourceSets.main.get().resources) + destinationDirectory.set(file(project.layout.buildDirectory).resolve("devlibs")) + } + + register("remapApiJar") { + dependsOn("apiJar") + archiveClassifier.set("api") + nestedJars.unset() + destinationDirectory.set(file(rootProject.layout.buildDirectory).resolve("api")) + + inputFile.set(apiJar.flatMap { it.archiveFile }) + } + + register("remapApiSourcesJar") { + dependsOn("apiSourcesJar") + archiveClassifier.set("api-sources") + destinationDirectory.set(file(rootProject.layout.buildDirectory).resolve("api-sources")) + + inputFile.set(apiSourcesJar.flatMap { it.archiveFile }) } remapJar { @@ -89,4 +137,32 @@ tasks { processResources { from(configurationCommonModResources) } +} + +publishing { + publications { + create("maven") { + groupId = project.group as String + artifactId = rootProject.name + "-" + project.name + version = version + + from(components["java"]) + } + + create("mavenApi") { + groupId = project.group as String + artifactId = rootProject.name + "-" + project.name + "-api" + version = version + + artifact(tasks.named("remapApiJar")) { + classifier = null + } + + artifact(tasks.named("remapApiSourcesJar")) { + classifier = "sources" + } + + pom.packaging = "jar" + } + } } \ No newline at end of file diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumFabricMod.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumFabricMod.java index 9aa1881b56..aefaba9638 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumFabricMod.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumFabricMod.java @@ -1,12 +1,15 @@ package net.caffeinemc.mods.sodium.fabric; import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; import net.caffeinemc.mods.sodium.client.render.frapi.SodiumRenderer; import net.caffeinemc.mods.sodium.client.util.FlawlessFrames; +import net.caffeinemc.mods.sodium.fabric.config.ConfigLoaderFabric; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.renderer.v1.RendererAccess; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; + import java.util.function.Consumer; public class SodiumFabricMod implements ClientModInitializer { @@ -19,6 +22,9 @@ public void onInitializeClient() { SodiumClientMod.onInitialization(mod.getMetadata().getVersion().getFriendlyString()); + ConfigLoaderFabric.collectConfigEntryPoints(); + ConfigManager.registerConfigsEarly(); + FabricLoader.getInstance() .getEntrypoints("frex_flawless_frames", Consumer.class) .forEach(api -> api.accept(FlawlessFrames.getProvider())); diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumPreLaunch.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumPreLaunch.java index 167edadf8b..473c5a6e8e 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumPreLaunch.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/SodiumPreLaunch.java @@ -8,9 +8,8 @@ public class SodiumPreLaunch implements PreLaunchEntrypoint { @Override public void onPreLaunch() { - PreLaunchChecks.beforeLWJGLInit(); + PreLaunchChecks.checkEnvironment(); GraphicsAdapterProbe.findAdapters(); - PreLaunchChecks.onGameInit(); Workarounds.init(); } } diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/block/FabricBlockAccess.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/block/FabricBlockAccess.java index 29069822a4..8d16c82968 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/block/FabricBlockAccess.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/block/FabricBlockAccess.java @@ -6,7 +6,6 @@ import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; import net.caffeinemc.mods.sodium.client.services.SodiumModelData; import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry; -import net.fabricmc.fabric.api.util.TriState; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.resources.model.BakedModel; @@ -78,7 +77,7 @@ public boolean platformHasBlockData() { @Override public float getNormalVectorShade(ModelQuadView quad, BlockAndTintGetter level, boolean shade) { - return normalShade(level, NormI8.unpackX(quad.getFaceNormal()), NormI8.unpackY(quad.getFaceNormal()), NormI8.unpackZ(quad.getFaceNormal()), shade); + return this.normalShade(level, NormI8.unpackX(quad.getFaceNormal()), NormI8.unpackY(quad.getFaceNormal()), NormI8.unpackZ(quad.getFaceNormal()), shade); } @Override @@ -90,4 +89,9 @@ public AmbientOcclusionMode usesAmbientOcclusion(BakedModel model, BlockState st public boolean shouldBlockEntityGlow(BlockEntity blockEntity, LocalPlayer player) { return false; } + + @Override + public boolean shouldOccludeFluid(Direction adjDirection, BlockState adjBlockState, FluidState fluid) { + return adjBlockState.getFluidState().getType().isSame(fluid.getType()); + } } diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java new file mode 100644 index 0000000000..8f33190c7c --- /dev/null +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.fabric.config; + +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.client.gui.SodiumConfigBuilder; +import net.fabricmc.loader.api.FabricLoader; + +public class ConfigLoaderFabric { + private static ConfigManager.ModMetadata getModMetadata(String modId) { + var mod = FabricLoader.getInstance().getModContainer(modId).orElseThrow(NullPointerException::new); + var metadata = mod.getMetadata(); + return new ConfigManager.ModMetadata(metadata.getName(), metadata.getVersion().getFriendlyString()); + } + + public static void collectConfigEntryPoints() { + ConfigManager.setModInfoFunction(ConfigLoaderFabric::getModMetadata); + + var entryPointContainers = FabricLoader.getInstance().getEntrypointContainers(ConfigManager.CONFIG_ENTRY_POINT_KEY, ConfigEntryPoint.class); + for (var container : entryPointContainers) { + ConfigManager.registerConfigEntryPoint(container::getEntrypoint, container.getProvider().getMetadata().getId()); + } + + ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, "sodium"); + } +} diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/level/FabricLevelAccess.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/level/FabricLevelAccess.java index 7f5fedf7c3..970963ea18 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/level/FabricLevelAccess.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/level/FabricLevelAccess.java @@ -1,27 +1,11 @@ package net.caffeinemc.mods.sodium.fabric.level; -import com.mojang.blaze3d.vertex.VertexConsumer; -import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; -import net.caffeinemc.mods.sodium.client.model.light.LightPipelineProvider; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.FluidRenderer; import net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess; -import net.caffeinemc.mods.sodium.client.world.LevelSlice; import net.caffeinemc.mods.sodium.client.world.SodiumAuxiliaryLightManager; -import net.caffeinemc.mods.sodium.fabric.render.FluidRendererImpl; -import net.minecraft.client.Camera; -import net.minecraft.client.renderer.LevelRenderer; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.culling.Frustum; -import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.chunk.LevelChunk; import org.jetbrains.annotations.Nullable; -import org.joml.Matrix4f; - -import java.util.List; -import java.util.function.Function; public class FabricLevelAccess implements PlatformLevelAccess { @Override diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FabricColorProviders.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FabricColorProviders.java index 28dec3befe..03dbb341ea 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FabricColorProviders.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FabricColorProviders.java @@ -22,7 +22,7 @@ public FabricFluidAdapter(FluidRenderHandler handler) { } @Override - public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, FluidState state, ModelQuadView quad, int[] output) { + public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, FluidState state, ModelQuadView quad, int[] output, boolean smooth) { Arrays.fill(output, 0xFF000000 | this.handler.getFluidColor(slice, pos, state)); } } diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FluidRendererImpl.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FluidRendererImpl.java index 3c37a1947f..f24d0e815d 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FluidRendererImpl.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/render/FluidRendererImpl.java @@ -33,8 +33,8 @@ public class FluidRendererImpl extends FluidRenderer { public FluidRendererImpl(ColorProviderRegistry colorProviderRegistry, LightPipelineProvider lighters) { this.colorProviderRegistry = colorProviderRegistry; - defaultRenderer = new DefaultFluidRenderer(lighters); - defaultContext = new DefaultRenderContext(); + this.defaultRenderer = new DefaultFluidRenderer(lighters); + this.defaultContext = new DefaultRenderContext(); } public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkBuildBuffers buffers) { @@ -61,7 +61,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat // // The default implementation of FluidRenderHandler#renderFluid invokes vanilla FluidRenderer#render, but // Fabric API does not support invoking vanilla FluidRenderer#render from FluidRenderHandler#renderFluid - // directly and it does not support calling the default implementation of FluidRenderHandler#renderFluid (super) + // directly, and it does not support calling the default implementation of FluidRenderHandler#renderFluid (super) // more than once. Because of this, the parameters to vanilla FluidRenderer#render will be the same as those // initially passed to FluidRenderHandler#renderFluid, so they can be ignored. // @@ -71,12 +71,12 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat // To allow invoking this method from the injector, where there is no local Sodium context, the renderer and // parameters are bundled into a DefaultRenderContext which is stored in a ThreadLocal. - defaultContext.setUp(this.colorProviderRegistry, this.defaultRenderer, level, blockState, fluidState, blockPos, offset, collector, meshBuilder, material, handler, hasModOverride); + this.defaultContext.setUp(this.colorProviderRegistry, this.defaultRenderer, level, blockState, fluidState, blockPos, offset, collector, meshBuilder, material, handler, hasModOverride); try { - FluidRendering.render(handler, level, blockPos, meshBuilder.asFallbackVertexConsumer(material, collector), blockState, fluidState, defaultContext); + FluidRendering.render(handler, level, blockPos, meshBuilder.asFallbackVertexConsumer(material, collector), blockState, fluidState, this.defaultContext); } finally { - defaultContext.clear(); + this.defaultContext.clear(); } } @@ -126,17 +126,17 @@ public void clear() { public ColorProvider getColorProvider(Fluid fluid) { var override = this.colorProviderRegistry.getColorProvider(fluid); - if (!hasModOverride && override != null) { + if (this.hasModOverride && override != null) { return override; } - return FabricColorProviders.adapt(handler); + return FabricColorProviders.adapt(this.handler); } @Override public void render(FluidRenderHandler handler, BlockAndTintGetter world, BlockPos pos, VertexConsumer vertexConsumer, BlockState blockState, FluidState fluidState) { this.renderer.render(this.level, this.blockState, this.fluidState, this.blockPos, this.offset, this.collector, this.meshBuilder, this.material, - getColorProvider(fluidState.getType()), handler.getFluidSprites(this.level, this.blockPos, this.fluidState)); + this.getColorProvider(fluidState.getType()), handler.getFluidSprites(this.level, this.blockPos, this.fluidState)); } } diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java index 22a9cbd4a0..98cf29d089 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java @@ -16,15 +16,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(BakedQuad.class) -public class BakedQuadMixin implements BakedQuadView { +public abstract class BakedQuadMixin implements BakedQuadView { @Shadow @Final protected int[] vertices; - @Shadow - @Final - protected TextureAtlasSprite sprite; - @Shadow @Final protected int tintIndex; @@ -84,10 +80,8 @@ public int getLight(int idx) { return this.vertices[ModelQuadUtil.vertexOffset(idx) + ModelQuadUtil.LIGHT_INDEX]; } - @Override - public TextureAtlasSprite getSprite() { - return this.sprite; - } + @Shadow + public abstract TextureAtlasSprite getSprite(); @Override public float getTexU(int idx) { diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java index 6fbc0d2ecf..4dae9e1cea 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java @@ -2,11 +2,12 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; import net.caffeinemc.mods.sodium.client.model.quad.BakedQuadView; import net.caffeinemc.mods.sodium.client.render.immediate.model.BakedModelEncoder; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils; import net.caffeinemc.mods.sodium.client.util.DirectionUtil; import net.minecraft.client.renderer.block.ModelBlockRenderer; @@ -46,7 +47,9 @@ private static void renderQuads(PoseStack.Pose matrices, VertexBufferWriter writ BakedModelEncoder.writeQuadVertices(writer, matrices, quad, color, light, overlay, false); - SpriteUtil.markSpriteActive(quad.getSprite()); + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } } } @@ -56,7 +59,7 @@ private static void renderQuads(PoseStack.Pose matrices, VertexBufferWriter writ */ @Inject(method = "renderModel", at = @At("HEAD"), cancellable = true) private void renderFast(PoseStack.Pose entry, VertexConsumer vertexConsumer, BlockState blockState, BakedModel bakedModel, float red, float green, float blue, int light, int overlay, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, EntityVertex.FORMAT); if (writer == null) { return; } diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java index 05c12f8caa..fb05e59cc0 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java @@ -38,12 +38,12 @@ public abstract class BiomeMixin { @Inject(method = "", at = @At("RETURN")) private void onInit(CallbackInfo ci) { - setupColors(); + this.setupColors(); } @Unique private void setupColors() { - this.cachedSpecialEffects = specialEffects; + this.cachedSpecialEffects = this.specialEffects; var grassColor = this.cachedSpecialEffects.getGrassColorOverride(); @@ -73,7 +73,7 @@ private void setupColors() { @Overwrite public int getGrassColor(double x, double z) { if (this.specialEffects != this.cachedSpecialEffects) { - setupColors(); + this.setupColors(); } int color; @@ -100,7 +100,7 @@ public int getGrassColor(double x, double z) { @Overwrite public int getFoliageColor() { if (this.specialEffects != this.cachedSpecialEffects) { - setupColors(); + this.setupColors(); } int color; diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 9b7fdda2c2..8c5872c288 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -6,7 +6,7 @@ "description" : "Sodium is a powerful rendering engine for Minecraft which greatly improves frame rates and micro-stutter, while fixing many graphical issues", "authors" : [ { - "name" : "@jellysquid3", + "name" : "JellySquid (jellysquid3)", "contact" : { "email" : "jellysquid@pm.me", "homepage" : "https://jellysquid.me" @@ -14,27 +14,28 @@ } ], "contributors" : [ - "@IMS212", - "@bytzo", - "@PepperCode1", - "@FlashyReese", - "@altrisi", - "@Grayray75", - "@Madis0", - "@Johni0702", - "@comp500", - "@coderbot16", - "@Moulberry", - "@MCRcortex", - "@Altirix", - "@embeddedt", - "@pajicadvance", - "@Kroppeb", - "@douira", - "@burgerindividual", - "@TwistedZero", - "@Leo40Git", - "@haykam821" + "IMS212", + "bytzo", + "PepperCode1", + "FlashyReese", + "altrisi", + "Grayray75", + "Madis0", + "Johni0702", + "comp500", + "coderbot16", + "Moulberry", + "MCRcortex", + "Altirix", + "embeddedt", + "pajicadvance", + "Kroppeb", + "douira", + "burgerindividual", + "TwistedZero", + "Leo40Git", + "haykam821", + "muzikbike" ], "contact" : { "homepage" : "https://github.com/CaffeineMC/sodium", @@ -71,7 +72,7 @@ ], "depends" : { "minecraft" : ["1.21", "1.21.1"], - "fabricloader" : ">=0.12.0", + "fabricloader" : ">=0.16.0", "fabric-block-view-api-v2" : "*", "fabric-renderer-api-v1" : "*", "fabric-rendering-data-attachment-v1" : "*", @@ -82,23 +83,32 @@ "embeddium": "*", "optifabric" : "*", "canvas" : "*", + "vulkanmod": "*", "sodium-blendingregistry" : "*", "ocrenderfix_sodium" : "*", "betterfpsdist" : "<=1.21-4.5", - "bobby" : "<=5.2.3", - "chunksfadein" : "<=1.0.1-1.21", + "bobby" : "<5.2.4", + "chunksfadein" : "<3.0.22-1.21", "cull-less-leaves" : "<=1.3.0", "cullleaves" : "<=3.4.0-fabric", "custom_hud" : "<3.4.2", "farsight" : "<=1.21-4.3", - "iceberg" : "<1.2.4", - "iris" : "<=1.7.3", + "iceberg" : "<1.2.7", + "iris" : "<1.8.13", "movingelevators" : "<=1.4.7", "notenoughcrashes" : "<4.4.8", - "noxesium" : "<2.1.4", - "reeses-sodium-options" : "<=1.7.3", - "sodium-extra" : "<=0.5.7", - "sspb" : "<=3.3.1" + "noxesium" : "<2.3.3", + "reeses-sodium-options" : "<1.8.0", + "sodium-extra" : "<0.8.7", + "sspb" : "<4.1.0", + "moreculling": "<1.0.8", + "simply-no-shading": "<7.6.2", + "betterend": "<=21.0.11", + "sodiumoptionsapi": "*", + "sodiumoptionsmodcompat": "*", + "sodiumextras": "*", + "sodiumdynamiclights": "*", + "sodiumleafculling": "*" }, "provides" : [ "indium" diff --git a/fabric/src/main/resources/sodium-fabric.mixins.json b/fabric/src/main/resources/sodium-fabric.mixins.json index 3ac3bf28ce..2bf1ee4ff3 100644 --- a/fabric/src/main/resources/sodium-fabric.mixins.json +++ b/fabric/src/main/resources/sodium-fabric.mixins.json @@ -1,12 +1,13 @@ { "package": "net.caffeinemc.mods.sodium.mixin", "required": true, - "compatibilityLevel": "JAVA_17", + "compatibilityLevel": "JAVA_21", "injectors": { "defaultRequire": 1 }, "overwrites": { - "conformVisibility": true + "conformVisibility": true, + "requireAnnotations": true }, "client": [ "core.model.quad.BakedQuadMixin", diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d..d997cfc60f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b..c61a118f7d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6b..0262dcbd52 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21834..c4bdd3ab8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index e2703e8483..967654a59f 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("multiloader-platform") - id("net.neoforged.moddev") version("2.0.42-beta") + id("net.neoforged.moddev") version("2.0.141") } base { @@ -9,14 +9,6 @@ base { } repositories { - maven("https://maven.pkg.github.com/ims212/Forge_Fabric_API") { - credentials { - username = "IMS212" - // Read only token - password = "ghp_" + "DEuGv0Z56vnSOYKLCXdsS9svK4nb9K39C1Hn" - } - } - maven("https://maven.su5ed.dev/releases") maven("https://maven.neoforged.net/releases/") } @@ -28,6 +20,12 @@ sourceSets { val configurationCommonModJava: Configuration = configurations.create("commonModJava") { isCanBeResolved = true } +val configurationCommonApiJava: Configuration = configurations.create("commonApiJava") { + isCanBeResolved = true +} +val configurationCommonApiSources: Configuration = configurations.create("apiSources") { + isCanBeResolved = true +} val configurationCommonModResources: Configuration = configurations.create("commonModResources") { isCanBeResolved = true } @@ -41,12 +39,16 @@ val configurationCommonServiceResources: Configuration = configurations.create(" dependencies { configurationCommonModJava(project(path = ":common", configuration = "commonMainJava")) - configurationCommonModJava(project(path = ":common", configuration = "commonApiJava")) - configurationCommonServiceJava(project(path = ":common", configuration = "commonEarlyLaunchJava")) + configurationCommonApiJava(project(path = ":common", configuration = "commonApiJava")) + configurationCommonServiceJava(project(path = ":common", configuration = "commonBootJava")) + + configurationCommonApiSources(project(path = ":common", configuration = "commonApiSources")) configurationCommonModResources(project(path = ":common", configuration = "commonMainResources")) configurationCommonModResources(project(path = ":common", configuration = "commonApiResources")) - configurationCommonServiceResources(project(path = ":common", configuration = "commonEarlyLaunchResources")) + configurationCommonServiceResources(project(path = ":common", configuration = "commonBootResources")) + + configurationCommonApiSources(project(path = ":common", configuration = "commonApiSources")) fun addEmbeddedFabricModule(dependency: String) { dependencies.implementation(dependency) @@ -54,47 +56,76 @@ dependencies { } addEmbeddedFabricModule("org.sinytra.forgified-fabric-api:fabric-api-base:0.4.42+d1308ded19") - addEmbeddedFabricModule("org.sinytra.forgified-fabric-api:fabric-renderer-api-v1:3.4.0+acb05a3919") + addEmbeddedFabricModule("org.sinytra.forgified-fabric-api:fabric-renderer-api-v1:3.4.1+9125b6dc19") addEmbeddedFabricModule("org.sinytra.forgified-fabric-api:fabric-rendering-data-attachment-v1:0.3.48+73761d2e19") addEmbeddedFabricModule("org.sinytra.forgified-fabric-api:fabric-block-view-api-v2:1.0.10+9afaaf8c19") - jarJar(project(":neoforge", "service")) + jarJar(project(":neoforge", "mod")) +} + +val modJar = tasks.register("modJar") { + from(configurationCommonModJava) + from(configurationCommonApiJava) + from(configurationCommonModResources) + + from(sourceSets["mod"].output) + + from(rootDir.resolve("LICENSE.md")) + + filesMatching(listOf("META-INF/neoforge.mods.toml")) { + expand(mapOf("version" to inputs.properties["version"])) + } + + archiveClassifier = "mod" } -val serviceJar = tasks.create("serviceJar") { - from(configurationCommonServiceJava) - from(configurationCommonServiceResources) +val apiJar = tasks.register("apiJar") { + from(configurationCommonApiJava) - from(sourceSets["service"].output) + from(rootDir.resolve("LICENSE.md")) + + archiveClassifier = "api" + + destinationDirectory.set(file(rootProject.layout.buildDirectory).resolve("api")) +} + +val apiSourcesJar = tasks.register("apiSourcesJar") { + from(configurationCommonApiSources) from(rootDir.resolve("LICENSE.md")) - manifest.attributes["FMLModType"] = "LIBRARY" + archiveClassifier = "api-sources" + + destinationDirectory.set(file(rootProject.layout.buildDirectory).resolve("api-sources")) +} - archiveClassifier = "service" +tasks.jar { + dependsOn(apiJar) } -val configurationService: Configuration = configurations.create("service") { +val configurationMod: Configuration = configurations.create("mod") { isCanBeConsumed = true isCanBeResolved = true outgoing { - artifact(serviceJar) + artifact(modJar) } } sourceSets { - named("service") { - compileClasspath = sourceSets["main"].compileClasspath - runtimeClasspath = sourceSets["main"].runtimeClasspath - + named("main") { compileClasspath += configurationCommonServiceJava runtimeClasspath += configurationCommonServiceJava } - main { + create("mod") { + compileClasspath = sourceSets["main"].compileClasspath + runtimeClasspath = sourceSets["main"].runtimeClasspath + compileClasspath += configurationCommonModJava + compileClasspath += configurationCommonApiJava runtimeClasspath += configurationCommonModJava + runtimeClasspath += configurationCommonApiJava } } @@ -117,26 +148,80 @@ neoForge { mods { create("sodium") { - sourceSet(sourceSets["main"]) + sourceSet(sourceSets["mod"]) sourceSet(project(":common").sourceSets["main"]) sourceSet(project(":common").sourceSets["api"]) } create("sodium-service") { - sourceSet(sourceSets["service"]) - sourceSet(project(":common").sourceSets["workarounds"]) + sourceSet(sourceSets["main"]) + sourceSet(project(":common").sourceSets["boot"]) } } } tasks { jar { - from(configurationCommonModJava) + from(configurationCommonServiceJava) + manifest.attributes["FMLModType"] = "LIBRARY" + manifest.attributes["Automatic-Module-Name"] = "sodium_service" + destinationDirectory.set(file(rootProject.layout.buildDirectory).resolve("mods")) + + from(sourceSets.getByName("mod").output.resourcesDir!!.resolve("META-INF/neoforge.mods.toml")) { + into("META-INF") + } + + from(project(":common").sourceSets.main.get().output.resourcesDir!!.resolve("sodium-icon.png")) } processResources { - from(configurationCommonModResources) + from(configurationCommonServiceResources) + } + + getByName("processModResources") { + filesMatching(listOf("META-INF/neoforge.mods.toml")) { + expand(mapOf("version" to BuildConfig.createVersionString(rootProject))) + } } } +publishing { + publications { + create("maven") { + groupId = project.group as String + artifactId = rootProject.name + "-" + project.name + version = version + + from(components["java"]) + } + + create("mavenApi") { + groupId = project.group as String + artifactId = rootProject.name + "-" + project.name + "-api" + version = version + + artifact(apiJar) { + classifier = null + } + + artifact(apiSourcesJar) { + classifier = "sources" + } + + pom.packaging = "jar" + } + + create("mavenMod") { + groupId = project.group as String + artifactId = rootProject.name + "-" + project.name + "-mod" + version = version + + artifact(modJar) { + classifier = null + } + + pom.packaging = "jar" + } + } +} diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java b/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java deleted file mode 100644 index ed7d5a42ac..0000000000 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.caffeinemc.mods.sodium.mixin.platform.neoforge; - -import net.caffeinemc.mods.sodium.client.render.frapi.render.AbstractBlockRenderContext; -import net.caffeinemc.mods.sodium.client.services.SodiumModelData; -import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; -import net.minecraft.client.renderer.RenderType; -import net.neoforged.neoforge.client.model.data.ModelData; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -@Mixin(AbstractBlockRenderContext.class) -public abstract class AbstractBlockRenderContextMixin implements RenderContext { - @Shadow - protected RenderType type; - - @Shadow - protected SodiumModelData modelData; - - @Override - public ModelData getModelData() { - return (ModelData) (Object) this.modelData; - } - - @Override - public RenderType getRenderType() { - return type; - } -} diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumServiceModLocator.java b/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumServiceModLocator.java new file mode 100644 index 0000000000..c03044da92 --- /dev/null +++ b/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumServiceModLocator.java @@ -0,0 +1,68 @@ +package net.caffeinemc.mods.sodium.service; + +import cpw.mods.jarhandling.JarContents; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.*; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Mounts the JiJ entries packaged inside the sodium service jar and hands them to FML's mod + * discovery pipeline. Refer to FML #375. + */ +public class SodiumServiceModLocator implements IModFileCandidateLocator { + private static final String JIJ_DIR = "META-INF/jarjar"; + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + Path jijDir = locateServiceRoot().resolve(JIJ_DIR); + try (Stream entries = Files.list(jijDir)) { + entries.filter(SodiumServiceModLocator::isJar).forEach(entry -> mountAndAdd(entry, pipeline)); + } catch (IOException e) { + throw new IllegalStateException("Failed to list JiJ entries in " + jijDir, e); + } + } + + @SuppressWarnings("resource") // innerFs ownership transfers to JarContents + private static void mountAndAdd(Path innerJarInsideService, IDiscoveryPipeline pipeline) { + try { + String specific = innerJarInsideService.toAbsolutePath().toUri().getRawSchemeSpecificPart(); + URI jijUri = new URI("jij:" + specific).normalize(); + FileSystem innerFs = FileSystems.newFileSystem(jijUri, Map.of("packagePath", innerJarInsideService)); + JarContents contents = JarContents.of(innerFs.getPath("/")); + pipeline.addJarContent(contents, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.WARN_ALWAYS); + } catch (URISyntaxException | IOException e) { + throw new IllegalStateException("Failed to add JiJ entry " + innerJarInsideService.getFileName() + " to mod discovery", e); + } + } + + private static boolean isJar(Path path) { + return path.getFileName().toString().toLowerCase().endsWith(".jar"); + } + + private static Path locateServiceRoot() { + var cs = SodiumServiceModLocator.class.getProtectionDomain().getCodeSource(); + if (cs == null || cs.getLocation() == null) { + throw new IllegalStateException("CodeSource unavailable; cannot resolve sodium service jar."); + } + URI csUri; + try { + csUri = cs.getLocation().toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Could not parse CodeSource location URI " + cs.getLocation(), e); + } + try { + return Paths.get(csUri); + } catch (Exception e) { + throw new IllegalStateException("Could not resolve sodium service jar from CodeSource URI " + csUri, e); + } + } +} diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java b/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java new file mode 100644 index 0000000000..d66993ebc9 --- /dev/null +++ b/neoforge/src/main/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java @@ -0,0 +1,30 @@ +package net.caffeinemc.mods.sodium.service; + +import net.caffeinemc.mods.sodium.client.compatibility.checks.PreLaunchChecks; +import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd.AmdWorkarounds; +import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; +import net.neoforged.fml.loading.FMLConfig; +import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; + +public class SodiumWorkarounds implements GraphicsBootstrapper { + @Override + public String name() { + return "sodium"; + } + + @Override + public void bootstrap(String[] arguments) { + PreLaunchChecks.checkEnvironment(); + GraphicsAdapterProbe.findAdapters(); + Workarounds.init(); + + // When early window control is disabled, NeoForge creates no early GL context, so context creation happens later in Window#createGlfwWindow. We want to avoid doing the workarounds twice if the context is going to be created later and thus our mixin to Window#createGlfwWindow runs that also applies them. + // See https://github.com/CaffeineMC/sodium/issues/3664 for more details. + if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_CONTROL)) { + NvidiaWorkarounds.applyEnvironmentChanges(); + AmdWorkarounds.applyEnvironmentChanges(); + } + } +} diff --git a/neoforge/src/service/resources/META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper b/neoforge/src/main/resources/META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper similarity index 100% rename from neoforge/src/service/resources/META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper rename to neoforge/src/main/resources/META-INF/services/net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper diff --git a/neoforge/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator b/neoforge/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator new file mode 100644 index 0000000000..8b2c36f004 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator @@ -0,0 +1 @@ +net.caffeinemc.mods.sodium.service.SodiumServiceModLocator diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java similarity index 96% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java index 63099265e6..8ddaa100a6 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/core/model/quad/BakedQuadMixin.java @@ -21,10 +21,6 @@ public abstract class BakedQuadMixin implements BakedQuadView { @Final protected int[] vertices; - @Shadow - @Final - protected TextureAtlasSprite sprite; - @Shadow @Final protected int tintIndex; @@ -86,10 +82,8 @@ public int getLight(int idx) { return this.vertices[ModelQuadUtil.vertexOffset(idx) + ModelQuadUtil.LIGHT_INDEX]; } - @Override - public TextureAtlasSprite getSprite() { - return this.sprite; - } + @Shadow + public abstract TextureAtlasSprite getSprite(); @Override public float getTexU(int idx) { diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java similarity index 96% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java index b625502653..e613d6aad4 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/model/MultiPartBakedModelMixin.java @@ -89,7 +89,7 @@ public List getQuads(@Nullable BlockState state, @Nullable Direction for (BakedModel model : models) { random.setSeed(seed); - if (canSkipRenderTypeCheck || renderType == null || model.getRenderTypes(state, random, modelData).contains(renderType)) { + if (this.canSkipRenderTypeCheck || renderType == null || model.getRenderTypes(state, random, modelData).contains(renderType)) { quads.addAll(model.getQuads(state, direction, random, MultipartModelData.resolve(modelData, model), renderType)); } } @@ -105,7 +105,7 @@ public List getQuads(@Nullable BlockState state, @Nullable Direction public ChunkRenderTypeSet getRenderTypes(@NotNull BlockState state, @NotNull RandomSource random, @NotNull ModelData data) { long seed = random.nextLong(); - if (canSkipRenderTypeCheck) { + if (this.canSkipRenderTypeCheck) { return ItemBlockRenderTypes.getRenderLayers(state); } diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/model/WeightedBakedModelMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/model/WeightedBakedModelMixin.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/model/WeightedBakedModelMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/model/WeightedBakedModelMixin.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java similarity index 93% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java index c87264a58e..02595a1463 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/render/model/block/ModelBlockRendererMixin.java @@ -2,11 +2,12 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; +import net.caffeinemc.mods.sodium.api.texture.SpriteUtil; import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.EntityVertex; import net.caffeinemc.mods.sodium.client.model.quad.BakedQuadView; import net.caffeinemc.mods.sodium.client.render.immediate.model.BakedModelEncoder; -import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils; import net.caffeinemc.mods.sodium.client.util.DirectionUtil; import net.minecraft.client.renderer.RenderType; @@ -48,7 +49,9 @@ private static void renderQuads(PoseStack.Pose matrices, VertexBufferWriter writ BakedModelEncoder.writeQuadVertices(writer, matrices, quad, color, light, overlay, false); - SpriteUtil.markSpriteActive(quad.getSprite()); + if (quad.getSprite() != null) { + SpriteUtil.INSTANCE.markSpriteActive(quad.getSprite()); + } } } @@ -58,7 +61,7 @@ private static void renderQuads(PoseStack.Pose matrices, VertexBufferWriter writ */ @Inject(method = "renderModel(Lcom/mojang/blaze3d/vertex/PoseStack$Pose;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/client/resources/model/BakedModel;FFFIILnet/neoforged/neoforge/client/model/data/ModelData;Lnet/minecraft/client/renderer/RenderType;)V", at = @At("HEAD"), cancellable = true) private void renderFast(PoseStack.Pose entry, VertexConsumer vertexConsumer, BlockState blockState, BakedModel bakedModel, float red, float green, float blue, int light, int overlay, ModelData modelData, RenderType renderType, CallbackInfo ci) { - var writer = VertexConsumerUtils.convertOrLog(vertexConsumer); + var writer = VertexConsumerUtils.convertOrLog(vertexConsumer, EntityVertex.FORMAT); if (writer == null) { return; } diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java similarity index 95% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java index cea4041f18..2cd742769a 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/features/world/biome/BiomeMixin.java @@ -38,12 +38,12 @@ public abstract class BiomeMixin { @Inject(method = "", at = @At("RETURN")) private void onInit(CallbackInfo ci) { - setupColors(); + this.setupColors(); } @Unique private void setupColors() { - this.cachedSpecialEffects = getModifiedSpecialEffects(); + this.cachedSpecialEffects = this.getModifiedSpecialEffects(); var grassColor = this.cachedSpecialEffects.getGrassColorOverride(); @@ -73,7 +73,7 @@ private void setupColors() { @Overwrite public int getGrassColor(double x, double z) { if (this.getModifiedSpecialEffects() != this.cachedSpecialEffects) { - setupColors(); + this.setupColors(); } int color; @@ -100,7 +100,7 @@ public int getGrassColor(double x, double z) { @Overwrite public int getFoliageColor() { if (this.getModifiedSpecialEffects() != this.cachedSpecialEffects) { - setupColors(); + this.setupColors(); } int color; diff --git a/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java new file mode 100644 index 0000000000..96767e6927 --- /dev/null +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AbstractBlockRenderContextMixin.java @@ -0,0 +1,67 @@ +package net.caffeinemc.mods.sodium.mixin.platform.neoforge; + +import net.caffeinemc.mods.sodium.client.render.frapi.render.AbstractBlockRenderContext; +import net.caffeinemc.mods.sodium.client.services.SodiumModelData; +import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.client.renderer.RenderType; +import net.neoforged.neoforge.client.model.data.ModelData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Deque; + +/** + * Self-mixin that implements {@link RenderContext} with NeoForge's {@link ModelData} based on + * {@link SodiumModelData} state in {@link AbstractBlockRenderContext}. + * + * See #3517. + */ +@Mixin(AbstractBlockRenderContext.class) +public abstract class AbstractBlockRenderContextMixin implements RenderContext { + @Shadow + protected RenderType type; + + @Shadow + protected SodiumModelData modelData; + + @Shadow + @Final + protected Deque modelDataStack; + + @Shadow + protected TriState useAO; + + @Override + public ModelData getModelData() { + SodiumModelData top = this.modelDataStack.peek(); + return (ModelData) (Object) (top != null ? top : this.modelData); + } + + @Override + public RenderType getRenderType() { + return this.type; + } + + @Override + public void pushModelData(ModelData modelData) { + // ModelData implements SodiumModelData via ModelDataMixin + this.modelDataStack.push((SodiumModelData) (Object) modelData); + } + + @Override + public void popModelData() { + this.modelDataStack.pop(); + } + + @Override + public TriState usesAmbientOcclusion() { + return this.useAO; + } + + @Override + public void setUsesAmbientOcclusion(TriState state) { + this.useAO = state; + } +} diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AuxiliaryLightManagerMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AuxiliaryLightManagerMixin.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AuxiliaryLightManagerMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/AuxiliaryLightManagerMixin.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ChunkRenderTypeSetAccessor.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ChunkRenderTypeSetAccessor.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ChunkRenderTypeSetAccessor.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ChunkRenderTypeSetAccessor.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ClientHooksMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ClientHooksMixin.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ClientHooksMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ClientHooksMixin.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java similarity index 63% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java index 5f5bf6f5d1..c32298fe7d 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/EntrypointMixin.java @@ -1,9 +1,12 @@ package net.caffeinemc.mods.sodium.mixin.platform.neoforge; import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.neoforge.config.ConfigLoaderForge; import net.minecraft.client.Minecraft; import net.minecraft.client.main.GameConfig; import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLLoader; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -17,5 +20,14 @@ public class EntrypointMixin { @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Options;loadSelectedResourcePacks(Lnet/minecraft/server/packs/repository/PackRepository;)V")) private void sodium$loadConfig(GameConfig gameConfig, CallbackInfo ci) { SodiumClientMod.onInitialization(ModList.get().getModContainerById("sodium").map(t -> t.getModInfo().getVersion().toString()).orElse("UNKNOWN")); + + // If any of these return false, FML is in a bad state. We can't continue like this. + if (FMLLoader.getLoadingModList().hasErrors() || !ModList.get().isLoaded("sodium")) { + SodiumClientMod.logger().error("Something has gone horribly wrong in mod loading; Sodium cannot continue."); + return; + } + + ConfigLoaderForge.collectConfigEntryPoints(); + ConfigManager.registerConfigsEarly(); } } diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java similarity index 87% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java index 42e8f89b9a..2be4524fcd 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/LevelSliceMixin.java @@ -43,7 +43,7 @@ public SodiumModelData getPlatformModelData(BlockPos pos) { @Override public ModelData getModelData(BlockPos pos) { - SodiumModelData modelData = getPlatformModelData(pos); + SodiumModelData modelData = this.getPlatformModelData(pos); return modelData != null ? (ModelData) (Object) modelData : null; } @@ -52,7 +52,7 @@ public ModelData getModelData(BlockPos pos) { int relChunkX = pos.x - (this.originBlockX >> 4); int relChunkZ = pos.z - (this.originBlockZ >> 4); - return (AuxiliaryLightManager) auxLightManager[getLocalSectionIndex(relChunkX, 0, relChunkZ)]; + return (AuxiliaryLightManager) this.auxLightManager[getLocalSectionIndex(relChunkX, 0, relChunkZ)]; } @Override @@ -61,7 +61,7 @@ public ModelData getModelData(BlockPos pos) { int relBlockY = pos.getY() - this.originBlockY; int relBlockZ = pos.getZ() - this.originBlockZ; - return (AuxiliaryLightManager) auxLightManager[getLocalSectionIndex(relBlockX >> 4, relBlockY >> 4, relBlockZ >> 4)]; + return (AuxiliaryLightManager) this.auxLightManager[getLocalSectionIndex(relBlockX >> 4, relBlockY >> 4, relBlockZ >> 4)]; } @Override diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ModelDataMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ModelDataMixin.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ModelDataMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ModelDataMixin.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ResourcePackLoaderMixin.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ResourcePackLoaderMixin.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ResourcePackLoaderMixin.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/ResourcePackLoaderMixin.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/SimpleBakedModelAccessor.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/SimpleBakedModelAccessor.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/SimpleBakedModelAccessor.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/mixin/platform/neoforge/SimpleBakedModelAccessor.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/ForgeMixinOverrides.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/ForgeMixinOverrides.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/ForgeMixinOverrides.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/ForgeMixinOverrides.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/NeoForgeRuntimeInformation.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/NeoForgeRuntimeInformation.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/NeoForgeRuntimeInformation.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/NeoForgeRuntimeInformation.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java similarity index 92% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java index d6bebd24c0..cdac002336 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/SodiumForgeMod.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.neoforge; -import net.caffeinemc.mods.sodium.client.gui.SodiumOptionsGUI; +import net.caffeinemc.mods.sodium.client.gui.VideoSettingsScreen; import net.caffeinemc.mods.sodium.client.render.frapi.SodiumRenderer; import net.caffeinemc.mods.sodium.client.util.FlawlessFrames; import net.fabricmc.fabric.api.renderer.v1.RendererAccess; @@ -19,7 +19,7 @@ @Mod(value = "sodium", dist = Dist.CLIENT) public class SodiumForgeMod { public SodiumForgeMod(IEventBus bus, ModContainer modContainer) { - modContainer.registerExtensionPoint(IConfigScreenFactory.class, (minecraft, screen) -> SodiumOptionsGUI.createScreen(screen)); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (minecraft, screen) -> VideoSettingsScreen.createScreen(screen)); RendererAccess.INSTANCE.registerRenderer(SodiumRenderer.INSTANCE); MethodHandles.Lookup lookup = MethodHandles.lookup(); diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java similarity index 92% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java index 978c2e8fe2..f857f6ab46 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/block/NeoForgeBlockAccess.java @@ -6,7 +6,6 @@ import net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess; import net.caffeinemc.mods.sodium.client.services.SodiumModelData; import net.caffeinemc.mods.sodium.client.util.DirectionUtil; -import net.fabricmc.fabric.api.util.TriState; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.resources.model.BakedModel; @@ -37,7 +36,7 @@ public boolean shouldShowFluidOverlay(BlockState block, BlockAndTintGetter level @Override public boolean platformHasBlockData() { - return false; + return true; } @Override @@ -58,4 +57,9 @@ public AmbientOcclusionMode usesAmbientOcclusion(BakedModel model, BlockState st public boolean shouldBlockEntityGlow(BlockEntity blockEntity, LocalPlayer player) { return blockEntity.hasCustomOutlineRendering(player); } + + @Override + public boolean shouldOccludeFluid(Direction adjDirection, BlockState adjBlockState, FluidState fluid) { + return adjBlockState.shouldHideAdjacentFluidFace(adjDirection, fluid); + } } diff --git a/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java new file mode 100644 index 0000000000..f0a4d5ba2f --- /dev/null +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java @@ -0,0 +1,71 @@ +package net.caffeinemc.mods.sodium.neoforge.config; + +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge; +import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; +import net.caffeinemc.mods.sodium.client.gui.SodiumConfigBuilder; +import net.neoforged.fml.ModList; +import net.neoforged.neoforgespi.language.IModInfo; +import org.objectweb.asm.Type; + +import java.lang.annotation.ElementType; + +/** + * Written with help from Contaria's implementation of this class. + */ +public class ConfigLoaderForge { + private static ConfigManager.ModMetadata getModMetadata(String modId) { + var mod = ModList.get().getModContainerById(modId).orElseThrow(() -> new + NullPointerException("Mod with id " + modId + " not found in ModList") + ).getModInfo(); + return new ConfigManager.ModMetadata(mod.getDisplayName(), mod.getVersion().toString()); + } + + public static void collectConfigEntryPoints() { + ConfigManager.setModInfoFunction(ConfigLoaderForge::getModMetadata); + + // collect entry points from modes that specify it in their properties + for (IModInfo mod : ModList.get().getMods()) { + var modId = mod.getModId(); + + if (modId.equals("sodium")) { + ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, modId); + } else { + Object modProperty = mod.getModProperties().get(ConfigManager.CONFIG_ENTRY_POINT_KEY); + if (modProperty == null) { + continue; + } + + if (!(modProperty instanceof String)) { + SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the value is of the wrong type: {}", modId, modProperty.getClass()); + continue; + } + + ConfigManager.registerConfigEntryPoint((String) modProperty, modId); + } + } + + // collect entry points from mods that specify it as an annotation + var entryPointAnnotationType = Type.getType(ConfigEntryPointForge.class); + for (var scanData : ModList.get().getAllScanData()) { + for (var annotation : scanData.getAnnotations()) { + if (annotation.targetType() == ElementType.TYPE && annotation.annotationType().equals(entryPointAnnotationType)) { + var className = annotation.clazz().getClassName(); + var modIdData = annotation.annotationData().get("value"); + if (modIdData == null) { + SodiumClientMod.logger().warn("Class '{}' has a sodium config api entry point annotation but didn't specify which mod it belongs to with the annotation's default parameter.", className); + continue; + } + + var modId = modIdData.toString(); + if (ModList.get().getModContainerById(modId).isEmpty()) { + SodiumClientMod.logger().warn("The mod with id '{}' that was provided as the owner of a sodium config api entry point annotation on class '{}' doesn't exist.", modId, className); + continue; + } + + ConfigManager.registerConfigEntryPoint(className, modId); + } + } + } + } +} diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java similarity index 84% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java index beedc67b4f..540957fe48 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelAccess.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess; import net.caffeinemc.mods.sodium.client.world.SodiumAuxiliaryLightManager; +import net.fabricmc.fabric.api.blockview.v2.RenderDataBlockEntity; import net.minecraft.core.SectionPos; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.chunk.LevelChunk; @@ -10,7 +11,7 @@ public class NeoForgeLevelAccess implements PlatformLevelAccess { @Override public @Nullable Object getBlockEntityData(BlockEntity blockEntity) { - return null; + return ((RenderDataBlockEntity) blockEntity).getRenderData(); } @Override diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelRenderHooks.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelRenderHooks.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelRenderHooks.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/level/NeoForgeLevelRenderHooks.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/model/NeoForgeModelAccess.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/model/NeoForgeModelAccess.java similarity index 100% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/model/NeoForgeModelAccess.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/model/NeoForgeModelAccess.java diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java similarity index 94% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java index 44e64c4540..d3e59fe310 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/FluidRendererImpl.java @@ -21,10 +21,8 @@ import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions; import net.neoforged.neoforge.client.textures.FluidSpriteCache; -import java.util.Objects; - public class FluidRendererImpl extends FluidRenderer { - // The current default context is set up before invoking FluidRenderHandler#renderFluid and cleared afterwards. + // The current default context is set up before invoking FluidRenderHandler#renderFluid and cleared afterward. private static final ThreadLocal CURRENT_DEFAULT_CONTEXT = ThreadLocal.withInitial(DefaultRenderContext::new); private final ColorProviderRegistry colorProviderRegistry; @@ -32,7 +30,7 @@ public class FluidRendererImpl extends FluidRenderer { public FluidRendererImpl(ColorProviderRegistry colorProviderRegistry, LightPipelineProvider lighters) { this.colorProviderRegistry = colorProviderRegistry; - defaultRenderer = new DefaultFluidRenderer(lighters); + this.defaultRenderer = new DefaultFluidRenderer(lighters); } public void render(LevelSlice level, BlockState blockState, FluidState fluidState, BlockPos blockPos, BlockPos offset, TranslucentGeometryCollector collector, ChunkBuildBuffers buffers) { @@ -52,7 +50,7 @@ public void render(LevelSlice level, BlockState blockState, FluidState fluidStat // // The default implementation of FluidRenderHandler#renderFluid invokes vanilla FluidRenderer#render, but // Fabric API does not support invoking vanilla FluidRenderer#render from FluidRenderHandler#renderFluid - // directly and it does not support calling the default implementation of FluidRenderHandler#renderFluid (super) + // directly, and it does not support calling the default implementation of FluidRenderHandler#renderFluid (super) // more than once. Because of this, the parameters to vanilla FluidRenderer#render will be the same as those // initially passed to FluidRenderHandler#renderFluid, so they can be ignored. // @@ -121,12 +119,12 @@ public ColorProvider getColorProvider(Fluid fluid) { return override; } - return ForgeColorProviders.adapt(handler); + return ForgeColorProviders.adapt(this.handler); } public void render() { this.renderer.render(this.level, this.blockState, this.fluidState, this.blockPos, this.offset, this.collector, this.meshBuilder, this.material, - getColorProvider(fluidState.getType()), FluidSpriteCache.getFluidSprites(level, blockPos, fluidState)); + this.getColorProvider(this.fluidState.getType()), FluidSpriteCache.getFluidSprites(this.level, this.blockPos, this.fluidState)); } } diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java similarity index 93% rename from neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java rename to neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java index 53e4348db0..c606013d7c 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java +++ b/neoforge/src/mod/java/net/caffeinemc/mods/sodium/neoforge/render/ForgeColorProviders.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.neoforge.render; -import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; import net.caffeinemc.mods.sodium.client.world.LevelSlice; @@ -23,7 +22,7 @@ public ForgeFluidAdapter(IClientFluidTypeExtensions handler) { } @Override - public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, FluidState state, ModelQuadView quad, int[] output) { + public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratchPos, FluidState state, ModelQuadView quad, int[] output, boolean smooth) { Arrays.fill(output,this.handler.getTintColor(state, slice, pos)); } } diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/mod/resources/META-INF/accesstransformer.cfg similarity index 100% rename from neoforge/src/main/resources/META-INF/accesstransformer.cfg rename to neoforge/src/mod/resources/META-INF/accesstransformer.cfg diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/mod/resources/META-INF/neoforge.mods.toml similarity index 63% rename from neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to neoforge/src/mod/resources/META-INF/neoforge.mods.toml index e12ce22ee8..3d2869ec81 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/mod/resources/META-INF/neoforge.mods.toml @@ -1,5 +1,5 @@ modLoader = "javafml" -loaderVersion = "*" +loaderVersion = "[4,)" license = "Polyform-Shield-1.0.0" [[mods]] @@ -10,9 +10,9 @@ displayName = "Sodium" logoFile = "sodium-icon.png" #optional -authors = "JellySquid, IMS212" +authors = "JellySquid (jellysquid3), IMS212" -credits = "@bytzo, @PepperCode1, @FlashyReese, @altrisi, @Grayray75, @Madis0, @Johni0702, @comp500, @coderbot16, @Moulberry, @MCRcortex, @Altirix, @embeddedt, @pajicadvance, @Kroppeb, @douira, @burgerindividual, @TwistedZero, @Leo40Git, @haykam821" +credits = "bytzo, PepperCode1, FlashyReese, altrisi, Grayray75, Madis0, Johni0702, comp500, coderbot16, Moulberry, MCRcortex, Altirix, embeddedt, pajicadvance, Kroppeb, douira, burgerindividual, TwistedZero, Leo40Git, haykam821, muzikbike" description = ''' Sodium is a powerful rendering engine for Minecraft which improves frame rates and reduces lag spikes. @@ -26,7 +26,14 @@ provides = ["indium"] [[dependencies.sodium]] modId = "minecraft" type = "required" -versionRange = "[1.21,1.21.1]" +versionRange = "1.21.1" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.sodium]] +modId = "neoforge" +type = "required" +versionRange = "[21.1.82,)" ordering = "NONE" side = "CLIENT" diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.FluidRendererFactory b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.FluidRendererFactory similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.FluidRendererFactory rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.FluidRendererFactory diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformBlockAccess diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelAccess diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformLevelRenderHooks diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformMixinOverrides b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformMixinOverrides similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformMixinOverrides rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformMixinOverrides diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformModelAccess b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformModelAccess similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformModelAccess rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformModelAccess diff --git a/neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation b/neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation similarity index 100% rename from neoforge/src/main/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation rename to neoforge/src/mod/resources/META-INF/services/net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation diff --git a/neoforge/src/main/resources/sodium-neoforge.mixins.json b/neoforge/src/mod/resources/sodium-neoforge.mixins.json similarity index 89% rename from neoforge/src/main/resources/sodium-neoforge.mixins.json rename to neoforge/src/mod/resources/sodium-neoforge.mixins.json index 3da278d67d..7657fd0a88 100644 --- a/neoforge/src/main/resources/sodium-neoforge.mixins.json +++ b/neoforge/src/mod/resources/sodium-neoforge.mixins.json @@ -1,12 +1,13 @@ { "package" : "net.caffeinemc.mods.sodium.mixin", "required" : true, - "compatibilityLevel" : "JAVA_17", + "compatibilityLevel" : "JAVA_21", "injectors" : { "defaultRequire" : 1 }, "overwrites" : { - "conformVisibility" : true + "conformVisibility" : true, + "requireAnnotations": true }, "client" : [ "core.model.quad.BakedQuadMixin", diff --git a/neoforge/src/service/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java b/neoforge/src/service/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java deleted file mode 100644 index 1d6876b8e2..0000000000 --- a/neoforge/src/service/java/net/caffeinemc/mods/sodium/service/SodiumWorkarounds.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.caffeinemc.mods.sodium.service; - -import net.caffeinemc.mods.sodium.client.compatibility.checks.PreLaunchChecks; -import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe; -import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; -import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds; -import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; - -public class SodiumWorkarounds implements GraphicsBootstrapper { - @Override - public String name() { - return "sodium"; - } - - @Override - public void bootstrap(String[] arguments) { - PreLaunchChecks.beforeLWJGLInit(); - GraphicsAdapterProbe.findAdapters(); - PreLaunchChecks.onGameInit(); - Workarounds.init(); - final boolean applyNvidiaWorkarounds = Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_THREADED_OPTIMIZATIONS); - - if (applyNvidiaWorkarounds) { - System.out.println("[Sodium] Applying NVIDIA workarounds earlier on Forge."); - NvidiaWorkarounds.install(); - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 304b9d9281..0f606fd359 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "sodium" pluginManagement { repositories { + mavenLocal() maven { url = uri("https://maven.fabricmc.net/") } maven { url = uri("https://maven.neoforged.net/releases/") } gradlePluginPortal()