Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Project exclude paths
|
||||||
|
/.gradle/
|
||||||
|
/build/
|
||||||
91
build.gradle
Normal file
91
build.gradle
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
plugins {
|
||||||
|
id 'fabric-loom' version '1.2-SNAPSHOT'
|
||||||
|
id 'maven-publish'
|
||||||
|
}
|
||||||
|
|
||||||
|
version = project.mod_version
|
||||||
|
group = project.maven_group
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
exclusiveContent {
|
||||||
|
forRepository {
|
||||||
|
maven {
|
||||||
|
name = "Modrinth"
|
||||||
|
url = "https://api.modrinth.com/maven"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter {
|
||||||
|
includeGroup "maven.modrinth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loom {
|
||||||
|
accessWidenerPath = file("src/main/resources/voxelmon.accesswidener")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// To change the versions see the gradle.properties file
|
||||||
|
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||||
|
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||||
|
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||||
|
|
||||||
|
modImplementation(fabricApi.module("fabric-api-base", project.fabric_version))
|
||||||
|
modImplementation(fabricApi.module("fabric-rendering-fluids-v1", project.fabric_version))
|
||||||
|
modImplementation(fabricApi.module("fabric-resource-loader-v0", project.fabric_version))
|
||||||
|
modImplementation("net.fabricmc.fabric-api:fabric-rendering-data-attachment-v1:0.3.38+73761d2e9a")
|
||||||
|
|
||||||
|
modImplementation "maven.modrinth:sodium:mc1.20.2-0.5.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def targetJavaVersion = 17
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
// ensure that the encoding is set to UTF-8, no matter what the system default is
|
||||||
|
// this fixes some edge cases with special characters not displaying correctly
|
||||||
|
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
|
||||||
|
// If Javadoc is generated, this must be specified in that task too.
|
||||||
|
it.options.encoding = "UTF-8"
|
||||||
|
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||||
|
it.options.release = targetJavaVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||||
|
if (JavaVersion.current() < javaVersion) {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||||
|
}
|
||||||
|
archivesBaseName = project.archives_base_name
|
||||||
|
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
|
||||||
|
// if it is present.
|
||||||
|
// If you remove this line, sources will not be generated.
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
from("LICENSE") {
|
||||||
|
rename { "${it}_${project.archivesBaseName}"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.ext.lwjglVersion = "3.3.3"
|
||||||
|
project.ext.lwjglNatives = "natives-windows"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion")
|
||||||
|
|
||||||
|
implementation "org.lwjgl:lwjgl"
|
||||||
|
include(implementation "org.lwjgl:lwjgl-lmdb")
|
||||||
|
include(implementation "org.lwjgl:lwjgl-zstd")
|
||||||
|
runtimeOnly "org.lwjgl:lwjgl:$lwjglVersion:natives-windows"
|
||||||
|
runtimeOnly "org.lwjgl:lwjgl:$lwjglVersion:natives-linux"
|
||||||
|
include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-windows")
|
||||||
|
include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-windows")
|
||||||
|
include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-linux")
|
||||||
|
include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-linux")
|
||||||
|
}
|
||||||
15
gradle.properties
Normal file
15
gradle.properties
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Done to increase the memory available to gradle.
|
||||||
|
org.gradle.jvmargs=-Xmx1G
|
||||||
|
|
||||||
|
# Fabric Properties
|
||||||
|
# check these on https://modmuss50.me/fabric.html
|
||||||
|
minecraft_version=1.20.2
|
||||||
|
yarn_mappings=1.20.2+build.1
|
||||||
|
loader_version=0.14.22
|
||||||
|
|
||||||
|
# Mod Properties
|
||||||
|
mod_version = 0.0.1
|
||||||
|
maven_group = me.cortex
|
||||||
|
archives_base_name = voxelmon
|
||||||
|
|
||||||
|
fabric_version=0.89.0+1.20.2
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
245
gradlew
vendored
Normal file
245
gradlew
vendored
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 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.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://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.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# 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" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
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 %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
9
settings.gradle
Normal file
9
settings.gradle
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = 'Fabric'
|
||||||
|
url = 'https://maven.fabricmc.net/'
|
||||||
|
}
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/java/me/cortex/voxelmon/Test.java
Normal file
62
src/main/java/me/cortex/voxelmon/Test.java
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package me.cortex.voxelmon;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.storage.LMDBInterface;
|
||||||
|
import me.cortex.voxelmon.core.world.storage.StorageBackend;
|
||||||
|
import me.cortex.voxelmon.importers.WorldImporter;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.MDB_NOLOCK;
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.MDB_NOSUBDIR;
|
||||||
|
|
||||||
|
public class Test {
|
||||||
|
public static void main1(String[] args) {
|
||||||
|
var dbi = new LMDBInterface.Builder()
|
||||||
|
.setMaxDbs(1)
|
||||||
|
.open("testdbdir.db", MDB_NOLOCK | MDB_NOSUBDIR)
|
||||||
|
.fetch();
|
||||||
|
dbi.setMapSize(1<<29);
|
||||||
|
var db = dbi.createDb(null);
|
||||||
|
db.transaction(obj->{
|
||||||
|
var key = ByteBuffer.allocateDirect(4);
|
||||||
|
var val = ByteBuffer.allocateDirect(1);
|
||||||
|
for (int i = 0; i < 1<<20; i++) {
|
||||||
|
key.putInt(0, i);
|
||||||
|
obj.put(key, val, 0);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
db.close();
|
||||||
|
dbi.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main2(String[] args) throws Exception {
|
||||||
|
var storage = new StorageBackend(new File("run/storagefile.db"));
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
new Thread(()->{
|
||||||
|
//storage.getSectionData(1143914312599863680L);
|
||||||
|
storage.setSectionData(1143914312599863680L, MemoryUtil.memAlloc(12345));
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
//storage.getSectionData(1143914312599863680L);
|
||||||
|
//storage.setSectionData(1143914312599863612L, ByteBuffer.allocateDirect(12345));
|
||||||
|
//storage.setSectionData(1143914312599863680L, ByteBuffer.allocateDirect(12345));
|
||||||
|
//storage.close();
|
||||||
|
|
||||||
|
System.out.println(storage.getIdMappings());
|
||||||
|
storage.putIdMapping(1, ByteBuffer.allocateDirect(12));
|
||||||
|
|
||||||
|
Thread.sleep(1000);
|
||||||
|
storage.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
//WorldEngine engine = new WorldEngine(new File("storagefile2.db"), 5);
|
||||||
|
WorldImporter importer = new WorldImporter(null, null);
|
||||||
|
//importer.importWorld(new File("run/saves/Drehmal 2.2 Apotheosis Beta - 1.0.0/region/"));
|
||||||
|
importer.importWorldAsyncStart(new File("D:\\PrismLauncher-Windows-MSVC-Portable-7.1\\instances\\1.20.1(3)\\.minecraft\\.bobby\\build.docm77.de\\-8149132374211427218\\minecraft\\overworld\\"));
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/main/java/me/cortex/voxelmon/Voxelmon.java
Normal file
4
src/main/java/me/cortex/voxelmon/Voxelmon.java
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package me.cortex.voxelmon;
|
||||||
|
|
||||||
|
public class Voxelmon {
|
||||||
|
}
|
||||||
188
src/main/java/me/cortex/voxelmon/core/DistanceTracker.java
Normal file
188
src/main/java/me/cortex/voxelmon/core/DistanceTracker.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package me.cortex.voxelmon.core;
|
||||||
|
|
||||||
|
//Contains the logic to determine what is loaded and at what LoD level, dispatches render changes
|
||||||
|
// also determines what faces are built etc
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.RenderTracker;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
|
||||||
|
import me.cortex.voxelmon.core.util.DebugUtil;
|
||||||
|
import me.cortex.voxelmon.core.util.RingUtil;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
|
||||||
|
//Can use ring logic
|
||||||
|
// i.e. when a player moves the rings of each lod change (how it was doing in the original attempt)
|
||||||
|
// also have it do directional quad culling and rebuild the chunk if needed (this shouldent happen very often) (the reason is to significantly reduce draw calls)
|
||||||
|
// make the rebuild range like +-5 chunks along each axis (that means at higher levels, should only need to rebuild like)
|
||||||
|
// 4 sections or something
|
||||||
|
public class DistanceTracker {
|
||||||
|
private final TransitionRing2D[] rings;
|
||||||
|
private final RenderTracker tracker;
|
||||||
|
public DistanceTracker(RenderTracker tracker, int rings) {
|
||||||
|
this.rings = new TransitionRing2D[rings+1];
|
||||||
|
this.tracker = tracker;
|
||||||
|
|
||||||
|
int DIST = 16;
|
||||||
|
|
||||||
|
this.rings[0] = new TransitionRing2D(5, DIST, (x,z)->{
|
||||||
|
if (true) return;
|
||||||
|
for (int y = -2; y < 10; y++) {
|
||||||
|
this.tracker.remLvl0(x, y, z);
|
||||||
|
}
|
||||||
|
}, (x, z) -> {
|
||||||
|
for (int y = -2; y < 10; y++) {
|
||||||
|
this.tracker.addLvl0(x, y, z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 1; i < rings; i++) {
|
||||||
|
int capRing = i;
|
||||||
|
this.rings[i] = new TransitionRing2D(5+i, DIST, (x, z) -> this.dec(capRing, x, z), (x, z) -> this.inc(capRing, x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inc(int lvl, int x, int z) {
|
||||||
|
for (int y = -2>>lvl; y < 10>>lvl; y++) {
|
||||||
|
this.tracker.inc(lvl, x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dec(int lvl, int x, int z) {
|
||||||
|
for (int y = -2>>lvl; y < 10>>lvl; y++) {
|
||||||
|
this.tracker.dec(lvl, x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//How it works is there are N ring zones (one zone for each lod boundary)
|
||||||
|
// the transition zone is what determines what lods are rendered etc (and it biases higher lod levels cause its easier)
|
||||||
|
// the transition zone is only ever checked when the player moves 1<<(4+lodlvl) blocks, its position is set
|
||||||
|
|
||||||
|
//if the center suddenly changes (say more than 1<<(7+lodlvl) block) then invalidate the entire ring and recompute
|
||||||
|
// the lod sections
|
||||||
|
public void setCenter(int x, int y, int z) {
|
||||||
|
for (var ring : rings) {
|
||||||
|
if (ring != null) {
|
||||||
|
ring.update(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: add a new class thing that can track the central axis point so that
|
||||||
|
// geometry can be rebuilt with new flags with correct facing geometry built
|
||||||
|
// (could also make it so that it emits 3x the amount of draw calls, but that seems very bad idea)
|
||||||
|
|
||||||
|
|
||||||
|
private interface Transition2DCallback {
|
||||||
|
void callback(int x, int z);
|
||||||
|
}
|
||||||
|
private static final class TransitionRing2D {
|
||||||
|
private final int triggerRangeSquared;
|
||||||
|
private final int shiftSize;
|
||||||
|
private final Transition2DCallback enter;
|
||||||
|
private final Transition2DCallback exit;
|
||||||
|
private final int[] cornerPoints;
|
||||||
|
private final int radius;
|
||||||
|
|
||||||
|
private int lastUpdateX;
|
||||||
|
private int lastUpdateZ;
|
||||||
|
|
||||||
|
private int currentX;
|
||||||
|
private int currentZ;
|
||||||
|
|
||||||
|
//Note radius is in shiftScale
|
||||||
|
private TransitionRing2D(int shiftSize, int radius, Transition2DCallback onEntry, Transition2DCallback onExit) {
|
||||||
|
//trigger just less than every shiftSize scale
|
||||||
|
this.triggerRangeSquared = 1<<((shiftSize<<1) - 1);
|
||||||
|
this.shiftSize = shiftSize;
|
||||||
|
this.enter = onEntry;
|
||||||
|
this.exit = onExit;
|
||||||
|
this.cornerPoints = RingUtil.generatingBoundingCorner2D(radius);
|
||||||
|
this.radius = radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long Prel(int x, int z) {
|
||||||
|
return (Integer.toUnsignedLong(this.currentZ + z)<<32)|Integer.toUnsignedLong(this.currentX + x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(int x, int z) {
|
||||||
|
int dx = this.lastUpdateX - x;
|
||||||
|
int dz = this.lastUpdateZ - z;
|
||||||
|
int distSquared = dx*dx + dz*dz;
|
||||||
|
if (distSquared < this.triggerRangeSquared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Update the last update position
|
||||||
|
this.lastUpdateX = x;
|
||||||
|
this.lastUpdateZ = z;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Compute movement if it happened
|
||||||
|
int nx = x>>this.shiftSize;
|
||||||
|
int nz = z>>this.shiftSize;
|
||||||
|
|
||||||
|
if (nx == this.currentX && nz == this.currentZ) {
|
||||||
|
//No movement
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//FIXME: not right, needs to only call load/unload on entry and exit, cause atm its acting like a loaded circle
|
||||||
|
|
||||||
|
Long2IntOpenHashMap ops = new Long2IntOpenHashMap();
|
||||||
|
|
||||||
|
|
||||||
|
int dir = nz<this.currentZ?-1:1;
|
||||||
|
while (nz != this.currentZ) {
|
||||||
|
for (int corner : this.cornerPoints) {
|
||||||
|
int cx = corner>>>16;
|
||||||
|
int cz = corner&0xFFFF;
|
||||||
|
|
||||||
|
ops.addTo(Prel( cx, cz+Math.max(0, dir)), dir);
|
||||||
|
ops.addTo(Prel( cx,-cz+Math.min(0, dir)),-dir);
|
||||||
|
if (cx != 0) {
|
||||||
|
ops.addTo(Prel(-cx, cz+Math.max(0, dir)), dir);
|
||||||
|
ops.addTo(Prel(-cx,-cz+Math.min(0, dir)),-dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//ops.addTo(Prel(0, this.radius+Math.max(0, dir)), dir);
|
||||||
|
//ops.addTo(Prel(0, -this.radius+Math.min(0, dir)), -dir);
|
||||||
|
|
||||||
|
this.currentZ += dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = nx<this.currentX?-1:1;
|
||||||
|
while (nx != this.currentX) {
|
||||||
|
|
||||||
|
for (int corner : this.cornerPoints) {
|
||||||
|
int cx = corner&0xFFFF;
|
||||||
|
int cz = corner>>>16;
|
||||||
|
|
||||||
|
ops.addTo(Prel( cx+Math.max(0, dir), cz), dir);
|
||||||
|
ops.addTo(Prel(-cx+Math.min(0, dir), cz),-dir);
|
||||||
|
if (cz != 0) {
|
||||||
|
ops.addTo(Prel(cx + Math.max(0, dir), -cz), dir);
|
||||||
|
ops.addTo(Prel(-cx + Math.min(0, dir), -cz), -dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentX += dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ops.forEach((pos,val)->{
|
||||||
|
if (val > 0) {
|
||||||
|
this.enter.callback((int) (long)pos, (int) (pos>>32));
|
||||||
|
}
|
||||||
|
if (val < 0) {
|
||||||
|
this.exit.callback((int) (long)pos, (int) (pos>>32));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ops.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/main/java/me/cortex/voxelmon/core/VoxelCore.java
Normal file
225
src/main/java/me/cortex/voxelmon/core/VoxelCore.java
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package me.cortex.voxelmon.core;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.Gl46FarWorldRenderer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.RenderTracker;
|
||||||
|
import me.cortex.voxelmon.core.rendering.SharedIndexBuffer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
|
||||||
|
import me.cortex.voxelmon.core.util.DebugUtil;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import me.cortex.voxelmon.core.util.RingUtil;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import me.cortex.voxelmon.core.world.other.BiomeColour;
|
||||||
|
import me.cortex.voxelmon.core.world.other.BlockStateColour;
|
||||||
|
import me.cortex.voxelmon.core.world.other.ColourResolver;
|
||||||
|
import me.cortex.voxelmon.importers.WorldImporter;
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.block.Blocks;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.render.Camera;
|
||||||
|
import net.minecraft.client.render.Frustum;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.util.math.Box;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
import net.minecraft.world.chunk.WorldChunk;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
//Core class that ingests new data from sources and updates the required systems
|
||||||
|
|
||||||
|
//3 primary services:
|
||||||
|
// ingest service: this takes in unloaded chunk events from the client, processes the chunk and critically also updates the lod view of the world
|
||||||
|
// render data builder service: this service builds the render data from build requests it also handles the collecting of build data for the selected region (only axis aligned single lod tasks)
|
||||||
|
// serialization service: serializes changed world data and ensures that the database and any loaded data are in sync such that the database can never be more updated than loaded data, also performs compression on serialization
|
||||||
|
|
||||||
|
//there are multiple subsystems
|
||||||
|
//player tracker system (determines what lods are loaded and used by the player)
|
||||||
|
//updating system (triggers render data rebuilds when something from the ingest service causes an LOD change)
|
||||||
|
//the render system simply renders what data it has, its responsable for gpu memory layouts in arenas and rendering in an optimal way, it makes no requests back to any of the other systems or services, it just applies render data updates
|
||||||
|
|
||||||
|
//There is strict forward only dataflow
|
||||||
|
//Ingest -> world engine -> raw render data -> render data
|
||||||
|
public class VoxelCore {
|
||||||
|
public static VoxelCore INSTANCE = new VoxelCore();
|
||||||
|
|
||||||
|
private final WorldEngine world;
|
||||||
|
private final DistanceTracker distanceTracker;
|
||||||
|
private final RenderGenerationService renderGen;
|
||||||
|
private final RenderTracker renderTracker;
|
||||||
|
private final AbstractFarWorldRenderer renderer;
|
||||||
|
|
||||||
|
|
||||||
|
public VoxelCore() {
|
||||||
|
//Trigger the shared index buffer loading
|
||||||
|
SharedIndexBuffer.INSTANCE.id();
|
||||||
|
this.renderer = new Gl46FarWorldRenderer();
|
||||||
|
this.world = new WorldEngine(new File("ethoslab.db"), 16, 5);//"hc9.db"//"storagefile.db"
|
||||||
|
|
||||||
|
this.renderTracker = new RenderTracker(this.world, this.renderer);
|
||||||
|
this.renderGen = new RenderGenerationService(this.world, this.renderTracker,4);
|
||||||
|
this.world.setRenderTracker(this.renderTracker);
|
||||||
|
this.renderTracker.setRenderGen(this.renderGen);
|
||||||
|
|
||||||
|
this.distanceTracker = new DistanceTracker(this.renderTracker, 5);
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Random r = new Random();
|
||||||
|
for (int ring = 0; ring < 5; ring++) {
|
||||||
|
for (int x = -32; x < 32; x++) {
|
||||||
|
for (int z = -32; z < 32; z++) {
|
||||||
|
if ((-16 < x && x < 16) && (-16 < z && z < 16)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var b = new MemoryBuffer(1000 * 8);
|
||||||
|
for (long j = 0; j < b.size; j += 8) {
|
||||||
|
MemoryUtil.memPutLong(b.address + j, r.nextLong());
|
||||||
|
}
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(ring, x, 2>>ring, z), b, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//WorldImporter importer = new WorldImporter(this.world, MinecraftClient.getInstance().world);
|
||||||
|
//importer.importWorldAsyncStart(new File("saves/Etho's LP Ep550/region"));
|
||||||
|
|
||||||
|
Set<Block> biomeTintableAllFaces = new HashSet<>(List.of(Blocks.OAK_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.VINE, Blocks.MANGROVE_LEAVES,
|
||||||
|
Blocks.TALL_GRASS, Blocks.LARGE_FERN));
|
||||||
|
|
||||||
|
biomeTintableAllFaces.add(Blocks.SPRUCE_LEAVES);
|
||||||
|
biomeTintableAllFaces.add(Blocks.BIRCH_LEAVES);
|
||||||
|
biomeTintableAllFaces.add(Blocks.PINK_PETALS);
|
||||||
|
biomeTintableAllFaces.addAll(List.of(Blocks.FERN, Blocks.GRASS, Blocks.POTTED_FERN));
|
||||||
|
Set<Block> biomeTintableUpFace = new HashSet<>(List.of(Blocks.GRASS_BLOCK));
|
||||||
|
|
||||||
|
Set<Block> waterTint = new HashSet<>(List.of(Blocks.WATER));
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (var state : this.world.getMapper().getBlockStates()) {
|
||||||
|
int tintMsk = 0;
|
||||||
|
if (biomeTintableAllFaces.contains(state.getBlock())) {
|
||||||
|
tintMsk |= (1<<6)-1;
|
||||||
|
}
|
||||||
|
if (biomeTintableUpFace.contains(state.getBlock())) {
|
||||||
|
tintMsk |= 1<<Direction.UP.getId();
|
||||||
|
}
|
||||||
|
if (waterTint.contains(state.getBlock())) {
|
||||||
|
tintMsk |= 1<<6;
|
||||||
|
}
|
||||||
|
this.renderer.enqueueUpdate(new BlockStateColour(i++, tintMsk, ColourResolver.resolveColour(state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
for (var biome : this.world.getMapper().getBiomes()) {
|
||||||
|
long dualColour = ColourResolver.resolveBiomeColour(biome);
|
||||||
|
this.renderer.enqueueUpdate(new BiomeColour(i++, (int) dualColour, (int) (dualColour>>32)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueIngest(WorldChunk worldChunk) {
|
||||||
|
this.world.ingestService.enqueueIngest(worldChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderSetup(Frustum frustum, Camera camera) {
|
||||||
|
this.distanceTracker.setCenter(camera.getBlockPos().getX(), camera.getBlockPos().getY(), camera.getBlockPos().getZ());
|
||||||
|
this.renderer.setupRender(frustum, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderOpaque(MatrixStack matrices, double cameraX, double cameraY, double cameraZ) {
|
||||||
|
matrices.push();
|
||||||
|
matrices.translate(-cameraX, -cameraY, -cameraZ);
|
||||||
|
DebugUtil.setPositionMatrix(matrices);
|
||||||
|
matrices.pop();
|
||||||
|
|
||||||
|
/*
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
for (int y = 0; y < Math.max(1, 10>>i); y++) {
|
||||||
|
for (int x = -32; x < 32; x++) {
|
||||||
|
for (int z = -32; z < 32; z++) {
|
||||||
|
if (-16 < x && x < 16 && -16 < z && z < 16) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var sec = this.world.getOrLoadAcquire(i, x, y, z);
|
||||||
|
this.renderGen.enqueueTask(sec);
|
||||||
|
sec.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
//DebugRenderUtil.renderAABB(new Box(0,100,0,1,101,1), 0,1,0,0.1f);
|
||||||
|
//DebugRenderUtil.renderAABB(new Box(1,100,1,2,101,2), 1,0,0,0.1f);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
int LEVEL = 4;
|
||||||
|
int SEC_Y = 1>>LEVEL;
|
||||||
|
int X = 47>>LEVEL;
|
||||||
|
int Z = 32>>LEVEL;
|
||||||
|
var section = world.getOrLoadAcquire(LEVEL,X,SEC_Y,Z);
|
||||||
|
var data = section.copyData();
|
||||||
|
int SCALE = 1<<LEVEL;
|
||||||
|
int Y_OFFSET = SEC_Y<<(5+LEVEL);
|
||||||
|
int X_OFFSET = X<<(5+LEVEL);
|
||||||
|
int Z_OFFSET = Z<<(5+LEVEL);
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
var point = data[WorldSection.getIndex(x,y,z)];
|
||||||
|
if (point != 0) {
|
||||||
|
//var colours = world.getMapper().getColours(point);
|
||||||
|
//int colour = colours[Direction.UP.getId()];
|
||||||
|
//DebugUtil.renderAABB(new Box(x*SCALE,y*SCALE+Y_OFFSET,z*SCALE,x*SCALE+SCALE,y*SCALE+SCALE+Y_OFFSET,z*SCALE+SCALE), colour|0xFF);
|
||||||
|
point >>>= 27;
|
||||||
|
DebugUtil.renderAABB(new Box(x*SCALE + X_OFFSET,y*SCALE+Y_OFFSET,z*SCALE+Z_OFFSET,x*SCALE+SCALE + X_OFFSET,y*SCALE+SCALE+Y_OFFSET,z*SCALE+SCALE+Z_OFFSET), (float) (point&7)/7,(float) ((point>>3)&7)/7,(float) ((point>>6)&7)/7,1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.release();
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
var points = RingUtil.generatingBoundingCorner2D(4);
|
||||||
|
for (var point : points) {
|
||||||
|
int x = point>>>16;
|
||||||
|
int y = point&0xFFFF;
|
||||||
|
DebugUtil.renderAABB(new Box(x,150,y,x+1,151,y+1), 1,1,0,1);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.renderer.renderFarAwayOpaque(matrices, cameraX, cameraY, cameraZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDebugInfo(List<String> debug) {
|
||||||
|
debug.add("");
|
||||||
|
debug.add("");
|
||||||
|
debug.add("VoxelCore");
|
||||||
|
debug.add("Ingest service tasks: " + this.world.ingestService.getTaskCount());
|
||||||
|
debug.add("Saving service tasks: " + this.world.savingService.getTaskCount());
|
||||||
|
debug.add("Render service tasks: " + this.renderGen.getTaskCount());
|
||||||
|
debug.add("Loaded cache sizes: " + Arrays.toString(this.world.getLoadedSectionCacheSizes()));
|
||||||
|
this.renderer.addDebugData(debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Note: when doing translucent rendering, only need to sort when generating the geometry, or when crossing into the center zone
|
||||||
|
// cause in 99.99% of cases the sections dont need to be sorted
|
||||||
|
// since they are AABBS crossing the normal is impossible without one of the axis being equal
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
try {this.renderGen.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||||
|
try {this.renderer.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||||
|
try {this.world.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/java/me/cortex/voxelmon/core/gl/GlBuffer.java
Normal file
28
src/main/java/me/cortex/voxelmon/core/gl/GlBuffer.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
|
||||||
|
import static org.lwjgl.opengl.GL44C.glBufferStorage;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glCreateBuffers;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glNamedBufferStorage;
|
||||||
|
|
||||||
|
public class GlBuffer extends TrackedObject {
|
||||||
|
public final int id;
|
||||||
|
private final long size;
|
||||||
|
public GlBuffer(long size, int flags) {
|
||||||
|
this.id = glCreateBuffers();
|
||||||
|
this.size = size;
|
||||||
|
glNamedBufferStorage(this.id, size, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
this.free0();
|
||||||
|
glDeleteBuffers(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long size() {
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/java/me/cortex/voxelmon/core/gl/GlFence.java
Normal file
32
src/main/java/me/cortex/voxelmon/core/gl/GlFence.java
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.GL32.*;
|
||||||
|
|
||||||
|
public class GlFence extends TrackedObject {
|
||||||
|
private final long fence;
|
||||||
|
private boolean signaled;
|
||||||
|
|
||||||
|
public GlFence() {
|
||||||
|
this.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean signaled() {
|
||||||
|
if (!this.signaled) {
|
||||||
|
int ret = glClientWaitSync(this.fence, 0, 0);
|
||||||
|
if (ret == GL_ALREADY_SIGNALED || ret == GL_CONDITION_SATISFIED) {
|
||||||
|
this.signaled = true;
|
||||||
|
} else if (ret != GL_TIMEOUT_EXPIRED) {
|
||||||
|
throw new IllegalStateException("Poll for fence failed, glError: " + glGetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.signaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
super.free0();
|
||||||
|
glDeleteSync(this.fence);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/me/cortex/voxelmon/core/gl/GlFramebuffer.java
Normal file
25
src/main/java/me/cortex/voxelmon/core/gl/GlFramebuffer.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBFramebufferObject.*;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glCreateFramebuffers;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glNamedFramebufferTexture;
|
||||||
|
|
||||||
|
public class GlFramebuffer extends TrackedObject {
|
||||||
|
public final int id;
|
||||||
|
public GlFramebuffer() {
|
||||||
|
this.id = glCreateFramebuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GlFramebuffer bind(int attachment, GlTexture texture) {
|
||||||
|
glNamedFramebufferTexture(this.id, attachment, texture.id, 0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
super.free0();
|
||||||
|
glDeleteFramebuffers(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_FLUSH_EXPLICIT_BIT;
|
||||||
|
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_UNSYNCHRONIZED_BIT;
|
||||||
|
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_WRITE_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
|
||||||
|
import static org.lwjgl.opengl.GL45C.*;
|
||||||
|
|
||||||
|
public class GlPersistentMappedBuffer extends TrackedObject {
|
||||||
|
public final int id;
|
||||||
|
private final long size;
|
||||||
|
private final long addr;
|
||||||
|
public GlPersistentMappedBuffer(long size, int flags) {
|
||||||
|
this.id = glCreateBuffers();
|
||||||
|
this.size = size;
|
||||||
|
glNamedBufferStorage(this.id, size, GL_CLIENT_STORAGE_BIT|GL_MAP_PERSISTENT_BIT|(flags&(GL_MAP_WRITE_BIT|GL_MAP_READ_BIT)));
|
||||||
|
this.addr = nglMapNamedBufferRange(this.id, 0, size, flags|GL_MAP_PERSISTENT_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
this.free0();
|
||||||
|
glUnmapBuffer(this.id);
|
||||||
|
glDeleteBuffers(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long size() {
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long addr() {
|
||||||
|
return this.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/java/me/cortex/voxelmon/core/gl/GlTexture.java
Normal file
34
src/main/java/me/cortex/voxelmon/core/gl/GlTexture.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBFramebufferObject.glDeleteFramebuffers;
|
||||||
|
import static org.lwjgl.opengl.ARBFramebufferObject.glGenFramebuffers;
|
||||||
|
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D;
|
||||||
|
import static org.lwjgl.opengl.GL11C.glDeleteTextures;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glCreateTextures;
|
||||||
|
import static org.lwjgl.opengl.GL45C.glTextureStorage2D;
|
||||||
|
|
||||||
|
public class GlTexture extends TrackedObject {
|
||||||
|
final int id;
|
||||||
|
private final int type;
|
||||||
|
public GlTexture(int type) {
|
||||||
|
this.id = glCreateTextures(type);
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GlTexture store(int format, int levels, int width, int height) {
|
||||||
|
if (this.type == GL_TEXTURE_2D) {
|
||||||
|
glTextureStorage2D(this.id, format, levels, width, height);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unknown texture type");
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
super.free0();
|
||||||
|
glDeleteTextures(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl.shader;
|
||||||
|
|
||||||
|
public interface IShaderProcessor {
|
||||||
|
String process(ShaderType type, String source);
|
||||||
|
}
|
||||||
108
src/main/java/me/cortex/voxelmon/core/gl/shader/Shader.java
Normal file
108
src/main/java/me/cortex/voxelmon/core/gl/shader/Shader.java
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl.shader;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
import org.lwjgl.opengl.GL20C;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.GL20.glDeleteProgram;
|
||||||
|
import static org.lwjgl.opengl.GL20.glUseProgram;
|
||||||
|
|
||||||
|
public class Shader extends TrackedObject {
|
||||||
|
private final int id;
|
||||||
|
private Shader(int program) {
|
||||||
|
id = program;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder make(IShaderProcessor processor) {
|
||||||
|
return new Builder(processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder make() {
|
||||||
|
return new Builder((aa,source)->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind() {
|
||||||
|
glUseProgram(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void free() {
|
||||||
|
super.free0();
|
||||||
|
glDeleteProgram(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final Map<ShaderType, String> sources = new HashMap<>();
|
||||||
|
private final IShaderProcessor processor;
|
||||||
|
private Builder(IShaderProcessor processor) {
|
||||||
|
this.processor = processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder add(ShaderType type, String id) {
|
||||||
|
this.addSource(type, ShaderLoader.parse(id));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addSource(ShaderType type, String source) {
|
||||||
|
this.sources.put(type, this.processor.process(type, source));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Shader compile() {
|
||||||
|
int program = GL20C.glCreateProgram();
|
||||||
|
int[] shaders = this.sources.entrySet().stream().mapToInt(a->createShader(a.getKey(), a.getValue())).toArray();
|
||||||
|
|
||||||
|
for (int i : shaders) {
|
||||||
|
GL20C.glAttachShader(program, i);
|
||||||
|
}
|
||||||
|
GL20C.glLinkProgram(program);
|
||||||
|
for (int i : shaders) {
|
||||||
|
GL20C.glDetachShader(program, i);
|
||||||
|
GL20C.glDeleteShader(i);
|
||||||
|
}
|
||||||
|
printProgramLinkLog(program);
|
||||||
|
verifyProgramLinked(program);
|
||||||
|
return new Shader(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void printProgramLinkLog(int program) {
|
||||||
|
String log = GL20C.glGetProgramInfoLog(program);
|
||||||
|
|
||||||
|
if (!log.isEmpty()) {
|
||||||
|
System.err.println(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyProgramLinked(int program) {
|
||||||
|
int result = GL20C.glGetProgrami(program, GL20C.GL_LINK_STATUS);
|
||||||
|
|
||||||
|
if (result != GL20C.GL_TRUE) {
|
||||||
|
throw new RuntimeException("Shader program linking failed, see log for details");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createShader(ShaderType type, String src) {
|
||||||
|
int shader = GL20C.glCreateShader(type.gl);
|
||||||
|
GL20C.glShaderSource(shader, src);
|
||||||
|
GL20C.glCompileShader(shader);
|
||||||
|
String log = GL20C.glGetShaderInfoLog(shader);
|
||||||
|
|
||||||
|
if (!log.isEmpty()) {
|
||||||
|
System.err.println(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = GL20C.glGetShaderi(shader, GL20C.GL_COMPILE_STATUS);
|
||||||
|
|
||||||
|
if (result != GL20C.GL_TRUE) {
|
||||||
|
GL20C.glDeleteShader(shader);
|
||||||
|
|
||||||
|
throw new RuntimeException("Shader compilation failed, see log for details");
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl.shader;
|
||||||
|
|
||||||
|
import me.jellysquid.mods.sodium.client.gl.shader.ShaderConstants;
|
||||||
|
import me.jellysquid.mods.sodium.client.gl.shader.ShaderParser;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class ShaderLoader {
|
||||||
|
public static String parse(String id) {
|
||||||
|
return ShaderParser.parseShader("#import <" + id + ">", ShaderConstants.builder().build());
|
||||||
|
//return me.jellysquid.mods.sodium.client.gl.shader.ShaderLoader.getShaderSource(new Identifier(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package me.cortex.voxelmon.core.gl.shader;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER;
|
||||||
|
import static org.lwjgl.opengl.GL20.GL_VERTEX_SHADER;
|
||||||
|
import static org.lwjgl.opengl.GL43C.GL_COMPUTE_SHADER;
|
||||||
|
import static org.lwjgl.opengl.NVMeshShader.GL_MESH_SHADER_NV;
|
||||||
|
import static org.lwjgl.opengl.NVMeshShader.GL_TASK_SHADER_NV;
|
||||||
|
|
||||||
|
public enum ShaderType {
|
||||||
|
VERTEX(GL_VERTEX_SHADER),
|
||||||
|
FRAGMENT(GL_FRAGMENT_SHADER),
|
||||||
|
COMPUTE(GL_COMPUTE_SHADER),
|
||||||
|
MESH(GL_MESH_SHADER_NV),
|
||||||
|
TASK(GL_TASK_SHADER_NV);
|
||||||
|
public final int gl;
|
||||||
|
ShaderType(int glEnum) {
|
||||||
|
gl = glEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
//NOTE: an idea on how to do it is so that any render section, we _keep_ aquired (yes this will be very memory intensive)
|
||||||
|
// could maybe tosomething else
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.Shader;
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.ShaderType;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.UploadStream;
|
||||||
|
import me.cortex.voxelmon.core.world.other.BiomeColour;
|
||||||
|
import me.cortex.voxelmon.core.world.other.BlockStateColour;
|
||||||
|
import net.minecraft.client.render.Camera;
|
||||||
|
import net.minecraft.client.render.Frustum;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import org.joml.FrustumIntersection;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
|
||||||
|
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
|
||||||
|
import static org.lwjgl.opengl.GL15.glBindBuffer;
|
||||||
|
import static org.lwjgl.opengl.GL30.*;
|
||||||
|
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||||
|
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
|
||||||
|
import static org.lwjgl.opengl.GL42.GL_COMMAND_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||||
|
import static org.lwjgl.opengl.GL43.*;
|
||||||
|
|
||||||
|
//can make it so that register the key of the sections we have rendered, then when a section changes and is registered,
|
||||||
|
// dispatch an update to the render section data builder which then gets consumed by the render system and updates
|
||||||
|
// the rendered data
|
||||||
|
|
||||||
|
//Contains all the logic to render the world and manage gpu memory
|
||||||
|
// processes section load,unload,update render data and renders the world each frame
|
||||||
|
|
||||||
|
|
||||||
|
//Todo: tinker with having the compute shader where each thread is a position to render? maybe idk
|
||||||
|
public abstract class AbstractFarWorldRenderer {
|
||||||
|
protected final int vao = glGenVertexArrays();
|
||||||
|
|
||||||
|
protected final GlBuffer uniformBuffer;
|
||||||
|
protected final GeometryManager geometry;
|
||||||
|
|
||||||
|
private final ConcurrentLinkedDeque<BlockStateColour> stateUpdateQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
private final ConcurrentLinkedDeque<BiomeColour> biomeUpdateQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
protected final GlBuffer stateDataBuffer;
|
||||||
|
protected final GlBuffer biomeDataBuffer;
|
||||||
|
protected final GlBuffer light = null;
|
||||||
|
|
||||||
|
|
||||||
|
//Current camera base level section position
|
||||||
|
protected int sx;
|
||||||
|
protected int sy;
|
||||||
|
protected int sz;
|
||||||
|
|
||||||
|
protected FrustumIntersection frustum;
|
||||||
|
|
||||||
|
public AbstractFarWorldRenderer() {
|
||||||
|
this.uniformBuffer = new GlBuffer(1024, 0);
|
||||||
|
//TODO: make these both dynamically sized
|
||||||
|
this.stateDataBuffer = new GlBuffer((1<<16)*28, 0);//Capacity for 1<<16 entries
|
||||||
|
this.biomeDataBuffer = new GlBuffer(512*4*2, 0);//capacity for 1<<9 entries
|
||||||
|
this.geometry = new GeometryManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void setupVao();
|
||||||
|
|
||||||
|
public void setupRender(Frustum frustum, Camera camera) {
|
||||||
|
this.frustum = frustum.frustumIntersection;
|
||||||
|
|
||||||
|
this.sx = camera.getBlockPos().getX() >> 5;
|
||||||
|
this.sy = camera.getBlockPos().getY() >> 5;
|
||||||
|
this.sz = camera.getBlockPos().getZ() >> 5;
|
||||||
|
|
||||||
|
//TODO: move this to a render function that is only called
|
||||||
|
// once per frame when using multi viewport mods
|
||||||
|
//it shouldent matter if its called multiple times a frame however, as its synced with fences
|
||||||
|
UploadStream.INSTANCE.tick();
|
||||||
|
|
||||||
|
|
||||||
|
this.geometry.uploadResults();
|
||||||
|
//Upload any block state changes
|
||||||
|
while (!this.stateUpdateQueue.isEmpty()) {
|
||||||
|
var stateUpdate = this.stateUpdateQueue.pop();
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.stateDataBuffer, stateUpdate.id()*28L, 28);
|
||||||
|
MemoryUtil.memPutInt(ptr, stateUpdate.biomeTintMsk()); ptr+=4;
|
||||||
|
for (int faceColour : stateUpdate.faceColours()) {
|
||||||
|
MemoryUtil.memPutInt(ptr, faceColour); ptr+=4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Upload any biome changes
|
||||||
|
while (!this.biomeUpdateQueue.isEmpty()) {
|
||||||
|
var biomeUpdate = this.biomeUpdateQueue.pop();
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.biomeDataBuffer, biomeUpdate.id()*8L, 8);
|
||||||
|
MemoryUtil.memPutInt(ptr, biomeUpdate.foliageColour()); ptr+=4;
|
||||||
|
MemoryUtil.memPutInt(ptr, biomeUpdate.waterColour()); ptr+=4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz);
|
||||||
|
|
||||||
|
public void enqueueUpdate(BlockStateColour stateColour) {
|
||||||
|
this.stateUpdateQueue.add(stateColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueUpdate(BiomeColour biomeColour) {
|
||||||
|
this.biomeUpdateQueue.add(biomeColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueResult(BuiltSectionGeometry result) {
|
||||||
|
this.geometry.enqueueResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDebugData(List<String> debug) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
glDeleteVertexArrays(this.vao);
|
||||||
|
this.geometry.free();
|
||||||
|
this.uniformBuffer.free();
|
||||||
|
this.stateDataBuffer.free();
|
||||||
|
this.biomeDataBuffer.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.BufferArena;
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.UploadStream;
|
||||||
|
import me.cortex.voxelmon.core.util.IndexUtil;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
|
||||||
|
public class GeometryManager {
|
||||||
|
private static final int SECTION_METADATA_SIZE = 32;
|
||||||
|
|
||||||
|
|
||||||
|
private record SectionMeta(long position, long opaqueGeometryPtr, int opaqueQuadCount, long translucentGeometryPtr, int translucentQuadCount) {
|
||||||
|
public void writeMetadata(long ptr) {
|
||||||
|
//THIS IS DUE TO ENDIANNESS and that we are splitting a long into 2 ints
|
||||||
|
MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4;
|
||||||
|
MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4;
|
||||||
|
|
||||||
|
|
||||||
|
MemoryUtil.memPutInt(ptr, (int) this.opaqueGeometryPtr); ptr += 4;
|
||||||
|
MemoryUtil.memPutInt(ptr, this.opaqueQuadCount); ptr += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConcurrentLinkedDeque<BuiltSectionGeometry> buildResults = new ConcurrentLinkedDeque<>();
|
||||||
|
|
||||||
|
private int sectionCount = 0;
|
||||||
|
private final Long2IntOpenHashMap pos2id = new Long2IntOpenHashMap();
|
||||||
|
private final LongArrayList id2pos = new LongArrayList();
|
||||||
|
private final ObjectArrayList<SectionMeta> sectionMetadata = new ObjectArrayList<>();
|
||||||
|
|
||||||
|
private final GlBuffer sectionMetaBuffer;
|
||||||
|
private final BufferArena geometryBuffer;
|
||||||
|
|
||||||
|
|
||||||
|
public GeometryManager() {
|
||||||
|
this.sectionMetaBuffer = new GlBuffer(1L << 23, 0);
|
||||||
|
this.geometryBuffer = new BufferArena((1L << 31) - 1024, 8);
|
||||||
|
this.pos2id.defaultReturnValue(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueResult(BuiltSectionGeometry sectionGeometry) {
|
||||||
|
this.buildResults.add(sectionGeometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SectionMeta createMeta(BuiltSectionGeometry geometry) {
|
||||||
|
long geometryPtr = this.geometryBuffer.upload(geometry.geometryBuffer);
|
||||||
|
|
||||||
|
//TODO: support translucent geometry
|
||||||
|
return new SectionMeta(geometry.position, geometryPtr, (int) (geometry.geometryBuffer.size/8), -1,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void freeMeta(SectionMeta meta) {
|
||||||
|
if (meta.opaqueGeometryPtr != -1) {
|
||||||
|
this.geometryBuffer.free(meta.opaqueGeometryPtr);
|
||||||
|
}
|
||||||
|
if (meta.translucentGeometryPtr != -1) {
|
||||||
|
this.geometryBuffer.free(meta.translucentGeometryPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void uploadResults() {
|
||||||
|
while (!this.buildResults.isEmpty()) {
|
||||||
|
var result = this.buildResults.pop();
|
||||||
|
boolean isDelete = result.geometryBuffer == null && result.translucentGeometryBuffer == null;
|
||||||
|
if (isDelete) {
|
||||||
|
int id = -1;
|
||||||
|
if ((id = this.pos2id.remove(result.position)) != -1) {
|
||||||
|
if (this.id2pos.getLong(id) != result.position) {
|
||||||
|
throw new IllegalStateException("Removed position id not the same requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta = this.sectionMetadata.get(id);
|
||||||
|
this.freeMeta(meta);
|
||||||
|
|
||||||
|
|
||||||
|
this.sectionCount--;
|
||||||
|
if (id == this.sectionCount) {
|
||||||
|
//if we are at the end of the array dont have to do anything (maybe just upload a blank data, just to be sure)
|
||||||
|
|
||||||
|
//Remove the last element
|
||||||
|
this.sectionMetadata.remove(id);
|
||||||
|
this.id2pos.removeLong(id);
|
||||||
|
} else {
|
||||||
|
long swapLodPos = this.id2pos.getLong(this.sectionCount);
|
||||||
|
this.pos2id.put(swapLodPos, id);
|
||||||
|
this.id2pos.set(id, swapLodPos);
|
||||||
|
//Remove from the lists
|
||||||
|
this.id2pos.removeLong(this.sectionCount);
|
||||||
|
var swapMeta = this.sectionMetadata.remove(this.sectionCount);
|
||||||
|
this.sectionMetadata.set(id, swapMeta);
|
||||||
|
if (swapMeta.position != swapLodPos) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long) SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||||
|
swapMeta.writeMetadata(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int id = -1;
|
||||||
|
if ((id = this.pos2id.get(result.position)) != -1) {
|
||||||
|
//Update the existing data
|
||||||
|
var meta = this.sectionMetadata.get(id);
|
||||||
|
if (meta.position != result.position) {
|
||||||
|
throw new IllegalStateException("Meta position != result position");
|
||||||
|
}
|
||||||
|
//Delete the old data
|
||||||
|
this.freeMeta(meta);
|
||||||
|
|
||||||
|
//Create the new meta
|
||||||
|
meta = this.createMeta(result);
|
||||||
|
this.sectionMetadata.set(id, meta);
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||||
|
meta.writeMetadata(ptr);
|
||||||
|
} else {
|
||||||
|
//Add to the end of the array
|
||||||
|
id = this.sectionCount++;
|
||||||
|
this.pos2id.put(result.position, id);
|
||||||
|
this.id2pos.add(result.position);
|
||||||
|
|
||||||
|
//Create the new meta
|
||||||
|
var meta = this.createMeta(result);
|
||||||
|
this.sectionMetadata.add(meta);
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||||
|
meta.writeMetadata(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Assert some invarients
|
||||||
|
if (this.id2pos.size() != this.sectionCount || this.sectionCount != this.pos2id.size()) {
|
||||||
|
throw new IllegalStateException("Invariants broken");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSectionCount() {
|
||||||
|
return this.sectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void free() {
|
||||||
|
this.sectionMetaBuffer.free();
|
||||||
|
this.geometryBuffer.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int geometryId() {
|
||||||
|
return this.geometryBuffer.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int metaId() {
|
||||||
|
return this.sectionMetaBuffer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public float getGeometryBufferUsage() {
|
||||||
|
return this.geometryBuffer.usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.platform.GlStateManager;
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.Shader;
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.ShaderType;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.UploadStream;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import me.cortex.voxelmon.mixin.joml.AccessFrustumIntersection;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
import org.joml.Vector4f;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
|
||||||
|
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||||
|
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
|
||||||
|
import static org.lwjgl.opengl.GL42.*;
|
||||||
|
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL43.*;
|
||||||
|
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||||
|
|
||||||
|
public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
|
||||||
|
private final Shader commandGen = Shader.make()
|
||||||
|
.add(ShaderType.COMPUTE, "voxelmon:lod/gl46/cmdgen.comp")
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
private final Shader lodShader = Shader.make()
|
||||||
|
.add(ShaderType.VERTEX, "voxelmon:lod/gl46/quads.vert")
|
||||||
|
.add(ShaderType.FRAGMENT, "voxelmon:lod/gl46/quads.frag")
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
private final GlBuffer glCommandBuffer = new GlBuffer(100_000*5*4, 0);
|
||||||
|
|
||||||
|
public Gl46FarWorldRenderer() {
|
||||||
|
super();
|
||||||
|
setupVao();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupVao() {
|
||||||
|
glBindVertexArray(this.vao);
|
||||||
|
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.glCommandBuffer.id);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE.id());
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.glCommandBuffer.id);
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.geometry.metaId());
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.stateDataBuffer.id);//State LUT
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.biomeDataBuffer.id);//Biome LUT
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, 0);//Lighting LUT
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
|
||||||
|
if (this.geometry.getSectionCount() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RenderLayer.getCutoutMipped().startDrawing();
|
||||||
|
//RenderSystem.enableBlend();
|
||||||
|
//RenderSystem.defaultBlendFunc();
|
||||||
|
|
||||||
|
this.updateUniformBuffer(stack, cx, cy, cz);
|
||||||
|
UploadStream.INSTANCE.commit();
|
||||||
|
|
||||||
|
glBindVertexArray(this.vao);
|
||||||
|
this.commandGen.bind();
|
||||||
|
glDispatchCompute((this.geometry.getSectionCount()+127)/128, 1, 1);
|
||||||
|
glMemoryBarrier(GL_COMMAND_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT|GL_UNIFORM_BARRIER_BIT);
|
||||||
|
|
||||||
|
this.lodShader.bind();
|
||||||
|
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, this.geometry.getSectionCount(), 0);
|
||||||
|
//ARBIndirectParameters.glMultiDrawElementsIndirectCountARB(
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT|GL_FRAMEBUFFER_BARRIER_BIT);
|
||||||
|
//TODO: add gpu occlusion culling here (after the lod drawing) (maybe, finish the rest of the PoC first)
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
RenderLayer.getCutoutMipped().endDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUniformBuffer(MatrixStack stack, double cx, double cy, double cz) {
|
||||||
|
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, this.uniformBuffer.size());
|
||||||
|
var mat = new Matrix4f(RenderSystem.getProjectionMatrix()).mul(stack.peek().getPositionMatrix());
|
||||||
|
var innerTranslation = new Vector3f((float) (cx-(this.sx<<5)), (float) (cy-(this.sy<<5)), (float) (cz-(this.sz<<5)));
|
||||||
|
mat.translate(-innerTranslation.x, -innerTranslation.y, -innerTranslation.z);
|
||||||
|
mat.getToAddress(ptr); ptr += 4*4*4;
|
||||||
|
MemoryUtil.memPutInt(ptr, this.sx); ptr += 4;
|
||||||
|
MemoryUtil.memPutInt(ptr, this.sy); ptr += 4;
|
||||||
|
MemoryUtil.memPutInt(ptr, this.sz); ptr += 4;
|
||||||
|
MemoryUtil.memPutInt(ptr, this.geometry.getSectionCount()); ptr += 4;
|
||||||
|
var planes = ((AccessFrustumIntersection)this.frustum).getPlanes();
|
||||||
|
for (var plane : planes) {
|
||||||
|
plane.getToAddress(ptr); ptr += 4*4;
|
||||||
|
}
|
||||||
|
innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
super.shutdown();
|
||||||
|
this.commandGen.free();
|
||||||
|
this.lodShader.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addDebugData(List<String> debug) {
|
||||||
|
debug.add("Geometry buffer usage: " + ((float)Math.round((this.geometry.getGeometryBufferUsage()*100000))/1000) + "%");
|
||||||
|
debug.add("Render Sections: " + this.geometry.getSectionCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.Shader;
|
||||||
|
import me.cortex.voxelmon.core.gl.shader.ShaderType;
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.UploadStream;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
|
||||||
|
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||||
|
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
|
||||||
|
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||||
|
import static org.lwjgl.opengl.GL42.*;
|
||||||
|
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL43.glDispatchCompute;
|
||||||
|
import static org.lwjgl.opengl.NVMeshShader.glDrawMeshTasksNV;
|
||||||
|
|
||||||
|
//TODO: make this a 2 phase culling system
|
||||||
|
// first phase renders the terrain, in the terrain task shader it also checks if the section was not visible in the frustum but now is
|
||||||
|
// and then renders it and marks it as being in the frustum
|
||||||
|
public class NvFarWorldRenderer extends AbstractFarWorldRenderer {
|
||||||
|
private final Shader primaryTerrainRaster = Shader.make()
|
||||||
|
.add(ShaderType.TASK, "voxelmon:lod/nvmesh/primary.task")
|
||||||
|
.add(ShaderType.MESH, "voxelmon:lod/nvmesh/primary.mesh")
|
||||||
|
.add(ShaderType.FRAGMENT, "voxelmon:lod/nvmesh/primary.frag")
|
||||||
|
.compile();
|
||||||
|
@Override
|
||||||
|
protected void setupVao() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
|
||||||
|
if (this.geometry.getSectionCount() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RenderLayer.getCutoutMipped().startDrawing();
|
||||||
|
|
||||||
|
UploadStream.INSTANCE.commit();
|
||||||
|
|
||||||
|
glBindVertexArray(this.vao);
|
||||||
|
this.primaryTerrainRaster.bind();
|
||||||
|
glDrawMeshTasksNV(0, this.geometry.getSectionCount());
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
|
||||||
|
RenderLayer.getCutoutMipped().endDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
super.shutdown();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.gl.GlFramebuffer;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlTexture;
|
||||||
|
|
||||||
|
public class PostProcessing {
|
||||||
|
private final GlFramebuffer framebuffer;
|
||||||
|
private GlTexture colour;
|
||||||
|
private GlTexture depth;
|
||||||
|
|
||||||
|
public PostProcessing() {
|
||||||
|
this.framebuffer = new GlFramebuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
|
||||||
|
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
|
||||||
|
//Tracks active sections, dispatches updates to the build system, everything related to rendering flows through here
|
||||||
|
public class RenderTracker {
|
||||||
|
private static final class ActiveSectionObject {
|
||||||
|
private int buildFlags;
|
||||||
|
}
|
||||||
|
private final WorldEngine world;
|
||||||
|
private RenderGenerationService renderGen;
|
||||||
|
private final AbstractFarWorldRenderer renderer;
|
||||||
|
|
||||||
|
//private final Long2ObjectOpenHashMap<Object> activeSections = new Long2ObjectOpenHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Long,Object> activeSections = new ConcurrentHashMap<>(50000,0.75f, 16);
|
||||||
|
private static final Object O = new Object();
|
||||||
|
|
||||||
|
|
||||||
|
public void setRenderGen(RenderGenerationService renderGen) {
|
||||||
|
this.renderGen = renderGen;
|
||||||
|
}
|
||||||
|
public RenderTracker(WorldEngine world, AbstractFarWorldRenderer renderer) {
|
||||||
|
this.world = world;
|
||||||
|
this.renderer = renderer;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var loader = new Thread(()->{
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int OX = 0;//-27;
|
||||||
|
int OZ = 0;//276;
|
||||||
|
int DROP = 48;
|
||||||
|
|
||||||
|
//Do ring rendering
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
for (int x = -DROP; x <= DROP; x++) {
|
||||||
|
for (int z = -DROP; z <= DROP; z++) {
|
||||||
|
int d = x*x+z*z;
|
||||||
|
if (d<(DROP/2-1)*(DROP/2) || d>DROP*DROP)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (int y = -3>>i; y < Math.max(1, 10 >> i); y++) {
|
||||||
|
var sec = this.world.getOrLoadAcquire(i, x + (OX>>(1+i)), y, z + (OZ>>(1+i)));
|
||||||
|
//this.renderGen.enqueueTask(sec);
|
||||||
|
sec.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (this.renderGen.getTaskCount() > 1000) {
|
||||||
|
Thread.sleep(50);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loader.setDaemon(true);
|
||||||
|
//loader.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Adds a lvl 0 section into the world renderer
|
||||||
|
public void addLvl0(int x, int y, int z) {
|
||||||
|
this.renderGen.enqueueTask(0, x, y, z);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(0, x, y, z), O);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Removes a lvl 0 section from the world renderer
|
||||||
|
public void remLvl0(int x, int y, int z) {
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(0, x, y, z));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(0, x, y, z), null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Increases from lvl-1 to lvl at the coordinates (which are in lvl space)
|
||||||
|
public void inc(int lvl, int x, int y, int z) {
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)));
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1));
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl, x, y, z), O);
|
||||||
|
|
||||||
|
//TODO: make a seperate object to hold the build data and link it with the location in a
|
||||||
|
// concurrent hashmap or something, this is so that e.g. the build data position
|
||||||
|
// can be updated
|
||||||
|
|
||||||
|
this.renderGen.enqueueTask(lvl, x, y, z);
|
||||||
|
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)), null, null));
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), null, null));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Decreases from lvl to lvl-1 at the coordinates (which are in lvl space)
|
||||||
|
public void dec(int lvl, int x, int y, int z) {
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)), O);
|
||||||
|
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), O);
|
||||||
|
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||||
|
|
||||||
|
this.renderer.enqueueResult(new BuiltSectionGeometry(lvl, x, y, z, null, null));
|
||||||
|
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1));
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1)+1);
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1));
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1)+1);
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1));
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1)+1);
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1));
|
||||||
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1)+1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Updates a sections direction mask (e.g. if the player goes past the axis, the chunk must be updated)
|
||||||
|
public void updateDirMask(int lvl, int x, int y, int z, int newMask) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Called by the world engine when a section gets dirtied
|
||||||
|
public void sectionUpdated(WorldSection section) {
|
||||||
|
//this.renderGen.enqueueTask(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
//called by the RenderGenerationService about built geometry, the RenderTracker checks if it can use the result (e.g. the LoD hasnt changed/still correct etc)
|
||||||
|
// and dispatches it to the renderer
|
||||||
|
// it also batch collects the geometry sections until all the geometry for an operation is collected, then it executes the operation, its removes flickering
|
||||||
|
public void processBuildResult(BuiltSectionGeometry section) {
|
||||||
|
//Check that we still want the section
|
||||||
|
if (this.activeSections.containsKey(section.position)) {
|
||||||
|
this.renderer.enqueueResult(section);
|
||||||
|
} else {
|
||||||
|
section.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBuildFlagsOrAbort(WorldSection section) {
|
||||||
|
var holder = this.activeSections.get(section.getKey());
|
||||||
|
int buildMask = 0;
|
||||||
|
if (holder != null) {
|
||||||
|
if (section.z< (((int)MinecraftClient.getInstance().cameraEntity.getPos().z)>>(5+section.lvl))+1) {
|
||||||
|
buildMask |= 1<< Direction.SOUTH.getId();
|
||||||
|
}
|
||||||
|
if (section.z>(((int)MinecraftClient.getInstance().cameraEntity.getPos().z)>>(5+section.lvl))-1) {
|
||||||
|
buildMask |= 1<<Direction.NORTH.getId();
|
||||||
|
}
|
||||||
|
if (section.x<(((int)MinecraftClient.getInstance().cameraEntity.getPos().x)>>(5+section.lvl))+1) {
|
||||||
|
buildMask |= 1<<Direction.EAST.getId();
|
||||||
|
}
|
||||||
|
if (section.x>(((int)MinecraftClient.getInstance().cameraEntity.getPos().x)>>(5+section.lvl))-1) {
|
||||||
|
buildMask |= 1<<Direction.WEST.getId();
|
||||||
|
}
|
||||||
|
buildMask |= 1<<Direction.UP.getId();
|
||||||
|
buildMask |= ((1<<6)-1)^(1);
|
||||||
|
}
|
||||||
|
return buildMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldStillBuild(int lvl, int x, int y, int z) {
|
||||||
|
return this.activeSections.containsKey(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.rendering.util.BufferArena;
|
||||||
|
import me.cortex.voxelmon.core.util.IndexUtil;
|
||||||
|
|
||||||
|
public class SharedIndexBuffer {
|
||||||
|
public static final SharedIndexBuffer INSTANCE = new SharedIndexBuffer();
|
||||||
|
|
||||||
|
private int commonIndexBufferOffset = -1;
|
||||||
|
private int commonIndexQuadCount;
|
||||||
|
|
||||||
|
private final BufferArena indexBuffer;
|
||||||
|
|
||||||
|
public SharedIndexBuffer() {
|
||||||
|
this.indexBuffer = new BufferArena((1L << 16)*(2*6), 2 * 6);
|
||||||
|
this.getSharedIndexBuffer(16380);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSharedIndexBuffer(int newQuadCount) {
|
||||||
|
if (this.commonIndexBufferOffset == -1 || this.commonIndexQuadCount < newQuadCount) {
|
||||||
|
if (this.commonIndexBufferOffset != -1) {
|
||||||
|
this.indexBuffer.free(this.commonIndexBufferOffset);
|
||||||
|
}
|
||||||
|
var buffer = IndexUtil.generateQuadIndices(newQuadCount);
|
||||||
|
long offset = this.indexBuffer.upload(buffer);
|
||||||
|
if (offset >= 1L<<31) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
this.commonIndexBufferOffset = (int) offset;
|
||||||
|
buffer.free();
|
||||||
|
this.commonIndexQuadCount = newQuadCount;
|
||||||
|
}
|
||||||
|
return this.commonIndexBufferOffset * 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int id() {
|
||||||
|
return this.indexBuffer.id();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.building;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import me.cortex.voxelmon.core.util.TrackedObject;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
|
||||||
|
public class BuiltSectionGeometry {
|
||||||
|
public final long position;
|
||||||
|
public final MemoryBuffer geometryBuffer;
|
||||||
|
public final MemoryBuffer translucentGeometryBuffer;
|
||||||
|
|
||||||
|
public BuiltSectionGeometry(int lvl, int x, int y, int z, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
|
||||||
|
this(WorldEngine.getWorldSectionId(lvl, x, y, z), geometryBuffer, translucentGeometryBuffer);
|
||||||
|
}
|
||||||
|
public BuiltSectionGeometry(long position, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
|
||||||
|
this.position = position;
|
||||||
|
this.geometryBuffer = geometryBuffer;
|
||||||
|
this.translucentGeometryBuffer = translucentGeometryBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void free() {
|
||||||
|
if (this.geometryBuffer != null) {
|
||||||
|
this.geometryBuffer.free();
|
||||||
|
}
|
||||||
|
if (this.translucentGeometryBuffer != null) {
|
||||||
|
this.translucentGeometryBuffer.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.building;
|
||||||
|
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.util.Mesher2D;
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mapper;
|
||||||
|
|
||||||
|
/*
|
||||||
|
8 - Light (can probably make it 3,3 bit lighting then i get 2 spare bits for other things)
|
||||||
|
|
||||||
|
8 - R
|
||||||
|
8 - G
|
||||||
|
8 - B
|
||||||
|
4 - A
|
||||||
|
|
||||||
|
5 - x
|
||||||
|
5 - y
|
||||||
|
5 - z
|
||||||
|
4 - w
|
||||||
|
4 - h
|
||||||
|
3 - face
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: might be able to fit it within 32 bits _very hackily_ (keep the same position data)
|
||||||
|
// but then have a per section LUT
|
||||||
|
|
||||||
|
|
||||||
|
//V2 QUAD FORMAT (enables animations to work)
|
||||||
|
/*
|
||||||
|
1 - spare
|
||||||
|
8 - light
|
||||||
|
9 - biome id
|
||||||
|
20 - block id
|
||||||
|
5 - x
|
||||||
|
5 - y
|
||||||
|
5 - z
|
||||||
|
4 - w
|
||||||
|
4 - h
|
||||||
|
3 - face
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class QuadFormat {
|
||||||
|
//Note: the encodedMeshedData is from the Mesher2D
|
||||||
|
public static int encodePosition(int face, int otherAxis, int encodedMeshedData) {
|
||||||
|
if (Mesher2D.getW(encodedMeshedData) > 16 || Mesher2D.getH(encodedMeshedData) > 16) {
|
||||||
|
throw new IllegalStateException("Width or height > 16");
|
||||||
|
}
|
||||||
|
int out = face;
|
||||||
|
out |= switch (face >> 1) {
|
||||||
|
case 0 ->
|
||||||
|
(Mesher2D.getX(encodedMeshedData) << 21) |
|
||||||
|
(otherAxis << 16) |
|
||||||
|
(Mesher2D.getZ(encodedMeshedData) << 11) |
|
||||||
|
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
|
||||||
|
((Mesher2D.getH(encodedMeshedData)-1) << 3);
|
||||||
|
|
||||||
|
case 1 ->
|
||||||
|
(Mesher2D.getX(encodedMeshedData) << 21) |
|
||||||
|
(Mesher2D.getZ(encodedMeshedData) << 16) |
|
||||||
|
(otherAxis << 11) |
|
||||||
|
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
|
||||||
|
((Mesher2D.getH(encodedMeshedData)-1) << 3);
|
||||||
|
|
||||||
|
case 2 ->
|
||||||
|
(otherAxis << 21) |
|
||||||
|
(Mesher2D.getX(encodedMeshedData) << 16) |
|
||||||
|
(Mesher2D.getZ(encodedMeshedData) << 11) |
|
||||||
|
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
|
||||||
|
((Mesher2D.getH(encodedMeshedData)-1) << 3);
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + (face >> 1));
|
||||||
|
};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: finish
|
||||||
|
public static long encode(Mapper mapper, long id, int encodedPosition) {
|
||||||
|
return ((id>>>27)<<26)|Integer.toUnsignedLong(encodedPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long encode(Mapper mapper, long id, int face, int other, int encodedMeshedData) {
|
||||||
|
return encode(mapper, id, encodePosition(face, other, encodedMeshedData));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.building;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import me.cortex.voxelmon.core.util.Mesher2D;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mapper;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: make this only emit quads that are facing the rebuild location
|
||||||
|
// HAVE IT AS FLAGS so that a range around the player can be built with all quads etc
|
||||||
|
// this will cut down on the amount of quads by an insane about and help alot overall
|
||||||
|
public class RenderDataFactory {
|
||||||
|
private final Mesher2D mesher = new Mesher2D(5,15);//15
|
||||||
|
private final LongArrayList outData = new LongArrayList(1000);
|
||||||
|
private final WorldEngine world;
|
||||||
|
public RenderDataFactory(WorldEngine world) {
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: MAKE a render cache that caches each WorldSection directional face generation, cause then can just pull that directly
|
||||||
|
// instead of needing to regen the entire thing
|
||||||
|
|
||||||
|
|
||||||
|
//section is already acquired and gets released by the parent
|
||||||
|
|
||||||
|
//buildMask in the lower 6 bits contains the faces to build, the next 6 bits are whether the edge face builds against
|
||||||
|
// its neigbor or not (0 if it does 1 if it doesnt (0 is default behavior))
|
||||||
|
public BuiltSectionGeometry generateMesh(WorldSection section, int buildMask) {
|
||||||
|
//TODO: to speed it up more, check like section.isEmpty() and stuff like that, have masks for if a slice/layer is entirly air etc
|
||||||
|
|
||||||
|
//TODO: instead of having it check its neighbors with the same lod level, compare against 1 level lower, this will prevent cracks and seams from
|
||||||
|
// appearing between lods
|
||||||
|
|
||||||
|
|
||||||
|
if (section.definitelyEmpty()) {
|
||||||
|
return new BuiltSectionGeometry(section.getKey(), null, null);
|
||||||
|
}
|
||||||
|
var data = section.copyData();
|
||||||
|
|
||||||
|
long[] connectedData = null;
|
||||||
|
int dirId = Direction.UP.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (y < 31) {
|
||||||
|
var up = data[WorldSection.getIndex(x, y + 1, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y == 31 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y + 1, section.z);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(x, 0, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(x, z, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), y, Mesher2D.getZ(quad))], 1, y, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirId = Direction.EAST.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (x < 31) {
|
||||||
|
var up = data[WorldSection.getIndex(x + 1, y, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x == 31 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x + 1, section.y, section.z);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(0, y, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(y, z, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(x, Mesher2D.getX(quad), Mesher2D.getZ(quad))], 5, x, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirId = Direction.SOUTH.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (z < 31) {
|
||||||
|
var up = data[WorldSection.getIndex(x, y, z + 1)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (z == 31 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z + 1);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(x, y, 0)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(x, y, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), Mesher2D.getZ(quad), z)], 3, z, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirId = Direction.WEST.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int x = 31; x != -1; x--) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (x != 0) {
|
||||||
|
var up = data[WorldSection.getIndex(x - 1, y, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x == 0 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x - 1, section.y, section.z);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(31, y, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(y, z, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(x, Mesher2D.getX(quad), Mesher2D.getZ(quad))], 4, x, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirId = Direction.NORTH.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int z = 31; z != -1; z--) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (z != 0) {
|
||||||
|
var up = data[WorldSection.getIndex(x, y, z - 1)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (z == 0 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z - 1);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(x, y, 31)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(x, y, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), Mesher2D.getZ(quad), z)], 2, z, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirId = Direction.DOWN.getId();
|
||||||
|
if ((buildMask&(1<<dirId))!=0) {
|
||||||
|
for (int y = 31; y != -1; y--) {
|
||||||
|
this.mesher.reset();
|
||||||
|
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
var self = data[WorldSection.getIndex(x, y, z)];
|
||||||
|
if (self == Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (y != 0) {
|
||||||
|
var up = data[WorldSection.getIndex(x, y - 1, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y == 0 && ((buildMask>>(6+dirId))&1) == 0) {
|
||||||
|
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
|
||||||
|
if (connectedData == null) {
|
||||||
|
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y - 1, section.z);
|
||||||
|
connectedData = connectedSection.copyData();
|
||||||
|
connectedSection.release();
|
||||||
|
}
|
||||||
|
var up = connectedData[WorldSection.getIndex(x, 31, z)];
|
||||||
|
if (up != Mapper.AIR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mesher.put(x, z, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quads = this.mesher.process();
|
||||||
|
for (int i = 0; i < quads.length; i++) {
|
||||||
|
var quad = quads[i];
|
||||||
|
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), y, Mesher2D.getZ(quad))], 0, y, quad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.outData.isEmpty()) {
|
||||||
|
return new BuiltSectionGeometry(section.getKey(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new MemoryBuffer(this.outData.size()*8L);
|
||||||
|
for (int i = 0; i < this.outData.size(); i++) {
|
||||||
|
MemoryUtil.memPutLong(output.address + i * 8L, this.outData.getLong(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outData.clear();
|
||||||
|
return new BuiltSectionGeometry(section.getKey(), output, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.building;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||||
|
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
|
||||||
|
import me.cortex.voxelmon.core.rendering.RenderTracker;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
import net.minecraft.world.chunk.WorldChunk;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class RenderGenerationService {
|
||||||
|
//TODO: make it accept either a section or section position and have a concurrent hashset to determine if
|
||||||
|
// a section is in the build queue
|
||||||
|
private record BuildTask(Supplier<WorldSection> sectionSupplier) {}
|
||||||
|
|
||||||
|
private volatile boolean running = true;
|
||||||
|
private final Thread[] workers;
|
||||||
|
|
||||||
|
private final ConcurrentLinkedDeque<BuildTask> taskQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
private final Semaphore taskCounter = new Semaphore(0);
|
||||||
|
|
||||||
|
private final WorldEngine world;
|
||||||
|
private final RenderTracker tracker;
|
||||||
|
|
||||||
|
public RenderGenerationService(WorldEngine world, RenderTracker tracker, int workers) {
|
||||||
|
this.world = world;
|
||||||
|
this.tracker = tracker;
|
||||||
|
|
||||||
|
this.workers = new Thread[workers];
|
||||||
|
for (int i = 0; i < workers; i++) {
|
||||||
|
this.workers[i] = new Thread(this::renderWorker);
|
||||||
|
this.workers[i].setDaemon(true);
|
||||||
|
this.workers[i].setName("Render generation service #" + i);
|
||||||
|
this.workers[i].start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: add a generated render data cache
|
||||||
|
private void renderWorker() {
|
||||||
|
//Thread local instance of the factory
|
||||||
|
var factory = new RenderDataFactory(this.world);
|
||||||
|
while (this.running) {
|
||||||
|
this.taskCounter.acquireUninterruptibly();
|
||||||
|
if (!this.running) break;
|
||||||
|
var task = this.taskQueue.pop();
|
||||||
|
var section = task.sectionSupplier.get();
|
||||||
|
if (section == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
section.assertNotFree();
|
||||||
|
int buildFlags = this.tracker.getBuildFlagsOrAbort(section);
|
||||||
|
if (buildFlags != 0) {
|
||||||
|
this.tracker.processBuildResult(factory.generateMesh(section, buildFlags));
|
||||||
|
}
|
||||||
|
section.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add a priority system, higher detail sections must always be updated before lower detail
|
||||||
|
// e.g. priorities NONE->lvl0 and lvl1 -> lvl0 over lvl0 -> lvl1
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: make it pass either a world section, _or_ coodinates so that the render thread has to do the loading of the sections
|
||||||
|
// not the calling method
|
||||||
|
|
||||||
|
//TODO: maybe make it so that it pulls from the world to stop the inital loading absolutly butt spamming the queue
|
||||||
|
// and thus running out of memory
|
||||||
|
|
||||||
|
//TODO: REDO THIS ENTIRE THING
|
||||||
|
// render tasks should not be bound to a WorldSection, instead it should be bound to either a WorldSection or
|
||||||
|
// an LoD position, the issue is that if we bound to a LoD position we loose all the info of the WorldSection
|
||||||
|
// like if its in the render queue and if we should abort building the render data
|
||||||
|
//1 proposal fix is a Long2ObjectLinkedOpenHashMap<WorldSection> which means we can abort if needed,
|
||||||
|
// also gets rid of dependency on a WorldSection (kinda)
|
||||||
|
public void enqueueTask(int lvl, int x, int y, int z) {
|
||||||
|
this.taskQueue.add(new BuildTask(()->{
|
||||||
|
if (this.tracker.shouldStillBuild(lvl, x, y, z)) {
|
||||||
|
return this.world.getOrLoadAcquire(lvl, x, y, z);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.taskCounter.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueTask(WorldSection section) {
|
||||||
|
//TODO: fixme! buildMask could have changed
|
||||||
|
//if (!section.inRenderQueue.getAndSet(true)) {
|
||||||
|
// //TODO: add a boolean for needsRenderUpdate that can be set to false if the section is no longer needed
|
||||||
|
// // to be rendered, e.g. LoD level changed so that lod is no longer being rendered
|
||||||
|
// section.acquire();
|
||||||
|
// this.taskQueue.add(new BuildTask(()->section));
|
||||||
|
// this.taskCounter.release();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTaskCount() {
|
||||||
|
return this.taskCounter.availablePermits();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
boolean anyAlive = false;
|
||||||
|
for (var worker : this.workers) {
|
||||||
|
anyAlive |= worker.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyAlive) {
|
||||||
|
System.err.println("Render gen workers already dead on shutdown! this is very very bad, check log for errors from this thread");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Wait for the ingest to finish
|
||||||
|
while (this.taskCounter.availablePermits() != 0) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shutdown
|
||||||
|
this.running = false;
|
||||||
|
this.taskCounter.release(1000);
|
||||||
|
//Wait for thread to join
|
||||||
|
try {
|
||||||
|
for (var worker : this.workers) {
|
||||||
|
worker.join();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {throw new RuntimeException(e);}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.util;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxelmon.core.util.AllocationArena;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
public class BufferArena {
|
||||||
|
private final long size;
|
||||||
|
private final int elementSize;
|
||||||
|
private final GlBuffer buffer;
|
||||||
|
private final AllocationArena allocationMap = new AllocationArena();
|
||||||
|
private long used;
|
||||||
|
|
||||||
|
public BufferArena(long capacity, int elementSize) {
|
||||||
|
if (capacity%elementSize != 0) {
|
||||||
|
throw new IllegalArgumentException("Capacity not a multiple of element size");
|
||||||
|
}
|
||||||
|
this.size = capacity;
|
||||||
|
this.elementSize = elementSize;
|
||||||
|
this.buffer = new GlBuffer(capacity, 0);
|
||||||
|
this.allocationMap.setLimit(capacity/elementSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long upload(MemoryBuffer buffer) {
|
||||||
|
if (buffer.size%this.elementSize!=0) {
|
||||||
|
throw new IllegalArgumentException("Buffer size not multiple of elementSize");
|
||||||
|
}
|
||||||
|
int size = (int) (buffer.size/this.elementSize);
|
||||||
|
long addr = this.allocationMap.alloc(size);
|
||||||
|
long uploadPtr = UploadStream.INSTANCE.upload(this.buffer, addr * this.elementSize, buffer.size);
|
||||||
|
MemoryUtil.memCopy(buffer.address, uploadPtr, buffer.size);
|
||||||
|
this.used += size;
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void free(long allocation) {
|
||||||
|
this.used -= this.allocationMap.free(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void free() {
|
||||||
|
this.buffer.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int id() {
|
||||||
|
this.buffer.assertNotFreed();
|
||||||
|
return this.buffer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float usage() {
|
||||||
|
return (float) ((double)this.used/(this.buffer.size()/this.elementSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package me.cortex.voxelmon.core.rendering.util;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlFence;
|
||||||
|
import me.cortex.voxelmon.core.gl.GlPersistentMappedBuffer;
|
||||||
|
import me.cortex.voxelmon.core.util.AllocationArena;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
|
|
||||||
|
import static me.cortex.voxelmon.core.util.AllocationArena.SIZE_LIMIT;
|
||||||
|
import static org.lwjgl.opengl.ARBDirectStateAccess.glCopyNamedBufferSubData;
|
||||||
|
import static org.lwjgl.opengl.ARBDirectStateAccess.glFlushMappedNamedBufferRange;
|
||||||
|
import static org.lwjgl.opengl.ARBMapBufferRange.*;
|
||||||
|
import static org.lwjgl.opengl.GL11.glFinish;
|
||||||
|
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||||
|
import static org.lwjgl.opengl.GL42C.GL_BUFFER_UPDATE_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT;
|
||||||
|
|
||||||
|
public class UploadStream {
|
||||||
|
private final AllocationArena allocationArena = new AllocationArena();
|
||||||
|
private final GlPersistentMappedBuffer uploadBuffer;
|
||||||
|
|
||||||
|
private final Deque<UploadFrame> frames = new ArrayDeque<>();
|
||||||
|
private final LongArrayList thisFrameAllocations = new LongArrayList();
|
||||||
|
private final Deque<UploadData> uploadList = new ArrayDeque<>();
|
||||||
|
private final LongArrayList flushList = new LongArrayList();
|
||||||
|
|
||||||
|
public UploadStream(long size) {
|
||||||
|
this.uploadBuffer = new GlPersistentMappedBuffer(size,GL_MAP_WRITE_BIT|GL_MAP_UNSYNCHRONIZED_BIT|GL_MAP_FLUSH_EXPLICIT_BIT);
|
||||||
|
this.allocationArena.setLimit(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long caddr = -1;
|
||||||
|
private long offset = 0;
|
||||||
|
public long upload(GlBuffer buffer, long destOffset, long size) {
|
||||||
|
if (size > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
long addr;
|
||||||
|
if (this.caddr == -1 || !this.allocationArena.expand(this.caddr, (int) size)) {
|
||||||
|
this.caddr = this.allocationArena.alloc((int) size);//TODO: replace with allocFromLargest
|
||||||
|
if (this.caddr == SIZE_LIMIT) {
|
||||||
|
this.commit();
|
||||||
|
int attempts = 10;
|
||||||
|
while (--attempts != 0 && this.caddr == SIZE_LIMIT) {
|
||||||
|
glFinish();
|
||||||
|
this.tick();
|
||||||
|
this.caddr = this.allocationArena.alloc((int) size);
|
||||||
|
}
|
||||||
|
if (this.caddr == SIZE_LIMIT) {
|
||||||
|
throw new IllegalStateException("Could not allocate memory segment big enough for upload even after force flush");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.flushList.add(this.caddr);
|
||||||
|
this.offset = size;
|
||||||
|
addr = this.caddr;
|
||||||
|
} else {//Could expand the allocation so just update it
|
||||||
|
addr = this.caddr + this.offset;
|
||||||
|
this.offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.caddr + size > this.uploadBuffer.size()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploadList.add(new UploadData(buffer, addr, destOffset, size));
|
||||||
|
|
||||||
|
return this.uploadBuffer.addr() + addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void commit() {
|
||||||
|
//First flush all the allocations and enqueue them to be freed
|
||||||
|
{
|
||||||
|
for (long alloc : flushList) {
|
||||||
|
glFlushMappedNamedBufferRange(this.uploadBuffer.id, alloc, this.allocationArena.getSize(alloc));
|
||||||
|
this.thisFrameAllocations.add(alloc);
|
||||||
|
}
|
||||||
|
this.flushList.clear();
|
||||||
|
}
|
||||||
|
glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
|
||||||
|
//Execute all the copies
|
||||||
|
for (var entry : this.uploadList) {
|
||||||
|
glCopyNamedBufferSubData(this.uploadBuffer.id, entry.target.id, entry.uploadOffset, entry.targetOffset, entry.size);
|
||||||
|
}
|
||||||
|
this.uploadList.clear();
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
|
||||||
|
this.caddr = -1;
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
this.commit();
|
||||||
|
if (!this.thisFrameAllocations.isEmpty()) {
|
||||||
|
this.frames.add(new UploadFrame(new GlFence(), new LongArrayList(this.thisFrameAllocations)));
|
||||||
|
this.thisFrameAllocations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!this.frames.isEmpty()) {
|
||||||
|
//Since the ordering of frames is the ordering of the gl commands if we encounter an unsignaled fence
|
||||||
|
// all the other fences should also be unsignaled
|
||||||
|
if (!this.frames.peek().fence.signaled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//Release all the allocations from the frame
|
||||||
|
var frame = this.frames.pop();
|
||||||
|
frame.allocations.forEach(allocationArena::free);
|
||||||
|
frame.fence.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record UploadFrame(GlFence fence, LongArrayList allocations) {}
|
||||||
|
private record UploadData(GlBuffer target, long uploadOffset, long targetOffset, long size) {}
|
||||||
|
|
||||||
|
//A upload instance instead of passing one around by reference
|
||||||
|
// MUST ONLY BE USED ON THE RENDER THREAD
|
||||||
|
public static final UploadStream INSTANCE = new UploadStream(1<<25);//32 mb upload buffer
|
||||||
|
|
||||||
|
}
|
||||||
179
src/main/java/me/cortex/voxelmon/core/util/AllocationArena.java
Normal file
179
src/main/java/me/cortex/voxelmon/core/util/AllocationArena.java
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
//FIXME: NOTE: if there is a free block of size > 2^30 EVERYTHING BREAKS, need to either increase size
|
||||||
|
// or automatically split and manage multiple blocks which is very painful
|
||||||
|
//OR instead of addr, defer to a long[] and use indicies
|
||||||
|
|
||||||
|
//TODO: replace the LongAVLTreeSet with a custom implementation that doesnt cause allocations when searching
|
||||||
|
// and see if something like a RBTree is any better
|
||||||
|
public class AllocationArena {
|
||||||
|
public static final long SIZE_LIMIT = -1;
|
||||||
|
|
||||||
|
private final int ADDR_BITS = 34;//This gives max size per allocation of 2^30 and max address of 2^39
|
||||||
|
private final int SIZE_BITS = 64 - ADDR_BITS;
|
||||||
|
private final long SIZE_MSK = (1L<<SIZE_BITS)-1;
|
||||||
|
private final long ADDR_MSK = (1L<<ADDR_BITS)-1;
|
||||||
|
private final LongRBTreeSet FREE = new LongRBTreeSet();//Size Address
|
||||||
|
private final LongRBTreeSet TAKEN = new LongRBTreeSet();//Address Size
|
||||||
|
|
||||||
|
private long sizeLimit = Long.MAX_VALUE;
|
||||||
|
private long totalSize;
|
||||||
|
//Flags
|
||||||
|
public boolean resized;//If the required memory of the entire buffer grew
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
public long allocFromLargest(int size) {//Allocates from the largest avalible block, this is useful for expanding later on
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public long alloc(int size) {//TODO: add alignment support
|
||||||
|
if (size == 0) throw new IllegalArgumentException();
|
||||||
|
//This is stupid, iterator is not inclusive
|
||||||
|
var iter = FREE.iterator(((long) size << ADDR_BITS)-1);
|
||||||
|
if (!iter.hasNext()) {//No free space for allocation
|
||||||
|
//Create new allocation
|
||||||
|
resized = true;
|
||||||
|
long addr = totalSize;
|
||||||
|
if (totalSize+size>sizeLimit) {
|
||||||
|
return SIZE_LIMIT;
|
||||||
|
}
|
||||||
|
totalSize += size;
|
||||||
|
TAKEN.add((addr<<SIZE_BITS)|((long) size));
|
||||||
|
return addr;
|
||||||
|
} else {
|
||||||
|
long slot = iter.nextLong();
|
||||||
|
iter.remove();
|
||||||
|
if ((slot >>> ADDR_BITS) == size) {//If the allocation and slot is the same size, just add it to the taken
|
||||||
|
TAKEN.add((slot<<SIZE_BITS)|(slot >>> ADDR_BITS));
|
||||||
|
} else {
|
||||||
|
TAKEN.add(((slot&ADDR_MSK)<<SIZE_BITS)|size);
|
||||||
|
FREE.add((((slot >>> ADDR_BITS)-size)<<ADDR_BITS)|((slot&ADDR_MSK)+size));
|
||||||
|
}
|
||||||
|
resized = false;
|
||||||
|
return slot&ADDR_MSK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int free(long addr) {//Returns size of freed memory
|
||||||
|
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
|
||||||
|
var iter = TAKEN.iterator(addr<<SIZE_BITS);//Dont need to include -1 as size != 0
|
||||||
|
long slot = iter.nextLong();
|
||||||
|
if (slot>>SIZE_BITS != addr) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
long size = slot&SIZE_MSK;
|
||||||
|
iter.remove();
|
||||||
|
|
||||||
|
//Note: if there is a previous entry, it means that it is guaranteed for the ending address to either
|
||||||
|
// be the addr, or indicate a free slot that needs to be merged
|
||||||
|
if (iter.hasPrevious()) {
|
||||||
|
long prevSlot = iter.previousLong();
|
||||||
|
long endAddr = (prevSlot>>>SIZE_BITS) + (prevSlot&SIZE_MSK);
|
||||||
|
if (endAddr != addr) {//It means there is a free slot that needs to get merged into
|
||||||
|
long delta = (addr - endAddr);
|
||||||
|
FREE.remove((delta<<ADDR_BITS)|endAddr);//Free the slot to be merged into
|
||||||
|
//Generate a new slot to get put into FREE
|
||||||
|
slot = (endAddr<<SIZE_BITS) | ((slot&SIZE_MSK) + delta);
|
||||||
|
}
|
||||||
|
iter.nextLong();//Need to reset the iter into its state
|
||||||
|
}//If there is no previous it means were at the start of the buffer, we might need to merge with block 0 if we are not block 0
|
||||||
|
else if (!FREE.isEmpty()) {// if free is not empty it means we must merge with block of free starting at 0
|
||||||
|
//if (addr != 0)//FIXME: this is very dodgy solution, if addr == 0 it means its impossible for there to be a previous element
|
||||||
|
if (FREE.remove(addr<<ADDR_BITS)) {//Attempt to remove block 0, this is very dodgy as it assumes block zero is 0 addr n size
|
||||||
|
slot = addr + size;//slot at address 0 and size of 0 block + new block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//If there is a next element it is guarenteed to either be the next block, or indicate that there is
|
||||||
|
// a block that needs to be merged into
|
||||||
|
if (iter.hasNext()) {
|
||||||
|
long nextSlot = iter.nextLong();
|
||||||
|
long endAddr = (slot>>>SIZE_BITS) + (slot&SIZE_MSK);
|
||||||
|
if (endAddr != nextSlot>>>SIZE_BITS) {//It means there is a memory block to be merged in FREE
|
||||||
|
long delta = ((nextSlot>>>SIZE_BITS) - endAddr);
|
||||||
|
FREE.remove((delta<<ADDR_BITS)|endAddr);
|
||||||
|
slot = (slot&(ADDR_MSK<<SIZE_BITS)) | ((slot&SIZE_MSK) + delta);
|
||||||
|
}
|
||||||
|
}// if there is no next block it means that we have reached the end of the allocation sections and we can shrink the buffer
|
||||||
|
else {
|
||||||
|
resized = true;
|
||||||
|
totalSize -= (slot&SIZE_MSK);
|
||||||
|
return (int) size;
|
||||||
|
}
|
||||||
|
|
||||||
|
resized = false;
|
||||||
|
//Need to swap around the slot to be in FREE format
|
||||||
|
slot = (slot>>>SIZE_BITS) | (slot<<ADDR_BITS);
|
||||||
|
FREE.add(slot);//Add the free slot into segments
|
||||||
|
return (int) size;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Attempts to expand an allocation, returns true on success
|
||||||
|
public boolean expand(long addr, int extra) {
|
||||||
|
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
|
||||||
|
var iter = TAKEN.iterator(addr<<SIZE_BITS);
|
||||||
|
if (!iter.hasNext()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long slot = iter.nextLong();
|
||||||
|
if (slot>>SIZE_BITS != addr) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
long updatedSlot = (slot & (ADDR_MSK << SIZE_BITS)) | ((slot & SIZE_MSK) + extra);
|
||||||
|
resized = false;
|
||||||
|
if (iter.hasNext()) {
|
||||||
|
long next = iter.nextLong();
|
||||||
|
long endAddr = (slot>>>SIZE_BITS)+(slot&SIZE_MSK);
|
||||||
|
long delta = (next>>>SIZE_BITS) - endAddr;
|
||||||
|
if (extra <= delta) {
|
||||||
|
FREE.remove((delta<<ADDR_BITS)|endAddr);//Should assert this
|
||||||
|
iter.previousLong();//FOR SOME REASON NEED TO DO IT TWICE I HAVE NO IDEA WHY
|
||||||
|
iter.previousLong();
|
||||||
|
iter.remove();//Remove the allocation so it can be updated
|
||||||
|
TAKEN.add(updatedSlot);//Update the taken allocation
|
||||||
|
if (extra != delta) {//More space than needed, need to add a new FREE block
|
||||||
|
FREE.add(((delta-extra)<<ADDR_BITS)|(endAddr+extra));
|
||||||
|
}
|
||||||
|
//else There is exactly enough free space, so removing the free block and updating the allocation is enough
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;//Not enough room to expand
|
||||||
|
}
|
||||||
|
} else {//We are at the end of the buffer, we can expand as we like
|
||||||
|
if (totalSize+extra>sizeLimit)//If expanding and we would exceed the size limit, dont resize
|
||||||
|
return false;
|
||||||
|
iter.remove();
|
||||||
|
TAKEN.add(updatedSlot);
|
||||||
|
totalSize += extra;
|
||||||
|
resized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize(long addr) {
|
||||||
|
addr &= ADDR_MSK;
|
||||||
|
var iter = TAKEN.iterator(addr << SIZE_BITS);
|
||||||
|
if (!iter.hasNext())
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
long slot = iter.nextLong();
|
||||||
|
if (slot>>SIZE_BITS != addr) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return slot&SIZE_MSK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLimit(long size) {
|
||||||
|
this.sizeLimit = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class ByteBufferBackedInputStream extends InputStream {
|
||||||
|
|
||||||
|
ByteBuffer buf;
|
||||||
|
|
||||||
|
public ByteBufferBackedInputStream(ByteBuffer buf) {
|
||||||
|
this.buf = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (!this.buf.hasRemaining()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.buf.get() & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int read(byte[] bytes, int off, int len) throws IOException {
|
||||||
|
if (!this.buf.hasRemaining()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
len = Math.min(len, this.buf.remaining());
|
||||||
|
this.buf.get(bytes, off, len);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/main/java/me/cortex/voxelmon/core/util/DebugUtil.java
Normal file
72
src/main/java/me/cortex/voxelmon/core/util/DebugUtil.java
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import net.minecraft.client.render.*;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.util.math.Box;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
|
||||||
|
public class DebugUtil {
|
||||||
|
private static Matrix4f positionMatrix = new Matrix4f().identity();
|
||||||
|
public static void setPositionMatrix(MatrixStack stack) {
|
||||||
|
positionMatrix = new Matrix4f(stack.peek().getPositionMatrix());
|
||||||
|
}
|
||||||
|
public static void renderAABB(Box aabb, int colour) {
|
||||||
|
renderAABB(aabb, (float)(colour>>>24)/255, (float)((colour>>>16)&0xFF)/255, (float)((colour>>>8)&0xFF)/255, (float)(colour&0xFF)/255);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void renderBox(int x, int y, int z, int size, float r, float g, float b) {
|
||||||
|
renderAABB(new Box(x, y, z, x+size, y+size, z+size), r, g, b, 1.0f);
|
||||||
|
}
|
||||||
|
public static void renderAABB(Box aabb, float r, float g, float b, float a) {
|
||||||
|
RenderSystem.setShaderFogEnd(9999999);
|
||||||
|
RenderSystem.setShader(GameRenderer::getPositionProgram);
|
||||||
|
RenderSystem.setShaderColor(r, g, b, a);
|
||||||
|
RenderSystem.enableBlend();
|
||||||
|
RenderSystem.disableCull();
|
||||||
|
RenderSystem.enableDepthTest();
|
||||||
|
//RenderSystem.depthMask(false);
|
||||||
|
|
||||||
|
var tessellator = RenderSystem.renderThreadTesselator();
|
||||||
|
var builder = tessellator.getBuffer();
|
||||||
|
builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION);
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
|
||||||
|
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
|
||||||
|
|
||||||
|
tessellator.draw();
|
||||||
|
|
||||||
|
RenderSystem.disableBlend();
|
||||||
|
RenderSystem.enableCull();
|
||||||
|
RenderSystem.disableDepthTest();
|
||||||
|
RenderSystem.setShaderColor(1,1,1,1);
|
||||||
|
RenderSystem.depthMask(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/me/cortex/voxelmon/core/util/IndexUtil.java
Normal file
25
src/main/java/me/cortex/voxelmon/core/util/IndexUtil.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
public class IndexUtil {
|
||||||
|
public static MemoryBuffer generateQuadIndices(int quadCount) {
|
||||||
|
if ((quadCount*4) >= 1<<16) {
|
||||||
|
throw new IllegalArgumentException("Quad count to large");
|
||||||
|
}
|
||||||
|
MemoryBuffer buffer = new MemoryBuffer(quadCount * 6L * 2);
|
||||||
|
long ptr = buffer.address;
|
||||||
|
for(int i = 0; i < quadCount*4; i += 4) {
|
||||||
|
MemoryUtil.memPutShort(ptr + (0*2), (short) i);
|
||||||
|
MemoryUtil.memPutShort(ptr + (1*2), (short) (i + 1));
|
||||||
|
MemoryUtil.memPutShort(ptr + (2*2), (short) (i + 2));
|
||||||
|
MemoryUtil.memPutShort(ptr + (3*2), (short) (i + 1));
|
||||||
|
MemoryUtil.memPutShort(ptr + (4*2), (short) (i + 3));
|
||||||
|
MemoryUtil.memPutShort(ptr + (5*2), (short) (i + 2));
|
||||||
|
|
||||||
|
ptr += 6 * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/me/cortex/voxelmon/core/util/MemoryBuffer.java
Normal file
24
src/main/java/me/cortex/voxelmon/core/util/MemoryBuffer.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
public class MemoryBuffer extends TrackedObject {
|
||||||
|
public final long address;
|
||||||
|
public final long size;
|
||||||
|
|
||||||
|
public MemoryBuffer(long size) {
|
||||||
|
this.size = size;
|
||||||
|
this.address = MemoryUtil.nmemAlloc(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cpyTo(long dst) {
|
||||||
|
super.assertNotFreed();
|
||||||
|
MemoryUtil.memCopy(this.address, dst, this.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void free() {
|
||||||
|
super.free0();
|
||||||
|
MemoryUtil.nmemFree(this.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/main/java/me/cortex/voxelmon/core/util/Mesher2D.java
Normal file
167
src/main/java/me/cortex/voxelmon/core/util/Mesher2D.java
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class Mesher2D {
|
||||||
|
private final int size;
|
||||||
|
private final int maxSize;
|
||||||
|
private final long[] data;
|
||||||
|
private final BitSet meshed;
|
||||||
|
private int[] quadCache;
|
||||||
|
public Mesher2D(int sizeBits, int maxSize) {
|
||||||
|
this.size = sizeBits;
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
this.data = new long[1<<(sizeBits<<1)];
|
||||||
|
this.meshed = new BitSet(1<<(sizeBits<<1));
|
||||||
|
this.quadCache = new int[128];
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getIdx(int x, int z) {
|
||||||
|
int M = (1<<this.size)-1;
|
||||||
|
if (x>M || z>M) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return ((z&M)<<this.size)|(x&M);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mesher2D put(int x, int z, long data) {
|
||||||
|
this.data[this.getIdx(x, z)] = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getX(int data) {
|
||||||
|
return (data>>24)&0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getZ(int data) {
|
||||||
|
return (data>>16)&0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getH(int data) {
|
||||||
|
return (data>>8)&0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getW(int data) {
|
||||||
|
return data&0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
private static int encodeQuad(int x, int z, int sx, int sz) {
|
||||||
|
return ((x&0xFF)<<24)|((z&0xFF)<<16)|((sx&0xFF)<<8)|((sz&0xFF)<<0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canMerge(int x, int z, long match) {
|
||||||
|
int id = this.getIdx(x, z);
|
||||||
|
return this.data[id] == match && !this.meshed.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] process() {
|
||||||
|
//TODO: replace this loop with a loop over a bitset of data that has been put into the mesher, have the this.meshed be removed
|
||||||
|
// and just clear the databitset when its meshed
|
||||||
|
|
||||||
|
int[] quads = this.quadCache;
|
||||||
|
int idxCount = 0;
|
||||||
|
|
||||||
|
//TODO: add different strategies/ways to mesh
|
||||||
|
for (int z = 0; z < 1<<this.size; z++) {
|
||||||
|
for (int x = 0; x < 1<<this.size; x++) {
|
||||||
|
int idx = this.getIdx(x,z);
|
||||||
|
long data = this.data[idx];
|
||||||
|
if (data == 0 || this.meshed.get(idx)) {
|
||||||
|
//if this position has been meshed or is empty, dont mesh and continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
boolean ex = x != ((1<<this.size)-1);
|
||||||
|
boolean ez = z != ((1<<this.size)-1);
|
||||||
|
int endX = x;
|
||||||
|
int endZ = z;
|
||||||
|
while (ex || ez) {
|
||||||
|
//Expand in the x direction
|
||||||
|
if (ex) {
|
||||||
|
if (endX + 1 > this.maxSize || endX+1 == (1 << this.size) - 1) {
|
||||||
|
ex = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ex) {
|
||||||
|
for (int tz = z; tz < endZ+1; tz++) {
|
||||||
|
if (!this.canMerge(endX + 1, tz, data)) {
|
||||||
|
ex = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ex) {
|
||||||
|
endX++;
|
||||||
|
}
|
||||||
|
if (ez) {
|
||||||
|
if (endZ + 1 > this.maxSize || endZ+1 == (1<<this.size)-1) {
|
||||||
|
ez = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ez) {
|
||||||
|
for (int tx = x; tx < endX+1; tx++) {
|
||||||
|
if (!this.canMerge(tx, endZ + 1, data)) {
|
||||||
|
ez = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ez) {
|
||||||
|
endZ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Mark the sections as meshed
|
||||||
|
for (int mx = x; mx <= endX; mx++) {
|
||||||
|
for (int mz = z; mz <= endZ; mz++) {
|
||||||
|
this.meshed.set(this.getIdx(mx, mz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int encodedQuad = encodeQuad(x, z, endX - x + 1, endZ - z + 1);
|
||||||
|
|
||||||
|
{
|
||||||
|
int pIdx = idxCount++;
|
||||||
|
if (pIdx == quads.length) {
|
||||||
|
var newArray = new int[quads.length + 64];
|
||||||
|
System.arraycopy(quads, 0, newArray, 0, quads.length);
|
||||||
|
quads = newArray;
|
||||||
|
}
|
||||||
|
quads[pIdx] = encodedQuad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var out = new int[idxCount];
|
||||||
|
System.arraycopy(quads, 0, out, 0, idxCount);
|
||||||
|
this.quadCache = quads;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
var r = new Random(123451);
|
||||||
|
int a = 0;
|
||||||
|
long total = 0;
|
||||||
|
for (int i = 0; i < 200000; i++) {
|
||||||
|
var mesh = new Mesher2D(5,16);
|
||||||
|
for (int j = 0; j < 512; j++) {
|
||||||
|
mesh.put(r.nextInt(32), r.nextInt(32), r.nextInt(100));
|
||||||
|
}
|
||||||
|
long s = System.nanoTime();
|
||||||
|
var result = mesh.process();
|
||||||
|
total += System.nanoTime() - s;
|
||||||
|
a += result.hashCode();
|
||||||
|
}
|
||||||
|
System.out.println(total/(1e+6));
|
||||||
|
System.out.println((double) (total/(1e+6))/200000);
|
||||||
|
//mesh.put(0,0,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.meshed.clear();
|
||||||
|
Arrays.fill(this.data, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
78
src/main/java/me/cortex/voxelmon/core/util/RingUtil.java
Normal file
78
src/main/java/me/cortex/voxelmon/core/util/RingUtil.java
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||||
|
|
||||||
|
public class RingUtil {
|
||||||
|
private static int computeR(int rd2, int a, int b) {
|
||||||
|
return rd2 - (a*a) - (b*b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int computeR(int rd2, int a) {
|
||||||
|
return rd2 - (a*a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int pack(int a, int b, int d) {
|
||||||
|
int m = ((1<<10)-1);
|
||||||
|
return (a&m)|((b&m)<<10)|(d<<20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int pack(int a, int b) {
|
||||||
|
int m = ((1<<16)-1);
|
||||||
|
return (a&m)|((b&m)<<16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] generateBoundingHalfSphere(int radius) {
|
||||||
|
IntArrayList points = new IntArrayList();
|
||||||
|
int rd2 = radius*radius;
|
||||||
|
//Generate full sphere points for each axis
|
||||||
|
for (int a = - radius; a <= radius; a++) {
|
||||||
|
for (int b = - radius; b <= radius; b++) {
|
||||||
|
//Basicly do a rearranged form of
|
||||||
|
// r^2 = x^2 + y^2 + z^2
|
||||||
|
int pd = computeR(rd2, a, b);
|
||||||
|
if (pd < 0) {//Cant have -ve space
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pd = (int) Math.sqrt(pd);
|
||||||
|
points.add(pack(a,b,pd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points.toIntArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] generateBoundingHalfCircle(int radius) {
|
||||||
|
IntArrayList points = new IntArrayList();
|
||||||
|
int rd2 = radius*radius;
|
||||||
|
//Generate full sphere points for each axis
|
||||||
|
for (int a = - radius; a <= radius; a++) {
|
||||||
|
//Basicly do a rearranged form of
|
||||||
|
// r^2 = x^2 + y^2
|
||||||
|
int pd = computeR(rd2, a);
|
||||||
|
if (pd < 0) {//Cant have -ve space
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pd = (int) Math.sqrt(pd);
|
||||||
|
points.add(pack(a,pd));
|
||||||
|
}
|
||||||
|
return points.toIntArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static int[] generatingBoundingCorner2D(int radius) {
|
||||||
|
IntOpenHashSet points = new IntOpenHashSet();
|
||||||
|
//Do 2 pass (x and y) to generate and cover all points
|
||||||
|
for (int i = 0; i <= radius; i++) {
|
||||||
|
int other = (int) Math.floor(Math.sqrt(radius*radius - i*i));
|
||||||
|
//add points (x,other) and (other,x) as it covers the full spectrum
|
||||||
|
points.add((i<<16)|other);
|
||||||
|
//points.add((other<<16)|i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.toIntArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package me.cortex.voxelmon.core.util;
|
||||||
|
|
||||||
|
import java.lang.ref.Cleaner;
|
||||||
|
|
||||||
|
public abstract class TrackedObject {
|
||||||
|
private final Ref ref;
|
||||||
|
public TrackedObject() {
|
||||||
|
this.ref = register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void free0() {
|
||||||
|
if (this.isFreed()) {
|
||||||
|
throw new IllegalStateException("Object " + this + " was double freed.");
|
||||||
|
}
|
||||||
|
this.ref.freedRef[0] = true;
|
||||||
|
this.ref.cleanable.clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void free();
|
||||||
|
|
||||||
|
public void assertNotFreed() {
|
||||||
|
if (isFreed()) {
|
||||||
|
throw new IllegalStateException("Object " + this + " should not be free, but is");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFreed() {
|
||||||
|
return this.ref.freedRef[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Ref(Cleaner.Cleanable cleanable, boolean[] freedRef) {}
|
||||||
|
|
||||||
|
private static final Cleaner cleaner = Cleaner.create();
|
||||||
|
public static Ref register(Object obj) {
|
||||||
|
String clazz = obj.getClass().getName();
|
||||||
|
Throwable trace = new Throwable();
|
||||||
|
trace.fillInStackTrace();
|
||||||
|
boolean[] freed = new boolean[1];
|
||||||
|
var clean = cleaner.register(obj, ()->{
|
||||||
|
if (!freed[0]) {
|
||||||
|
System.err.println("Object named: "+ clazz+" was not freed, location at:\n" + trace);
|
||||||
|
System.err.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Ref(clean, freed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.cortex.voxelmon.core.voxelization;
|
||||||
|
|
||||||
|
public interface I3dByteSupplier {
|
||||||
|
byte supply(int x, int y, int z);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.cortex.voxelmon.core.voxelization;
|
||||||
|
|
||||||
|
public interface I3dSupplier <T> {
|
||||||
|
T supply(int x, int y, int z);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package me.cortex.voxelmon.core.voxelization;
|
||||||
|
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mapper;
|
||||||
|
|
||||||
|
//16x16x16 block section
|
||||||
|
public class VoxelizedSection {
|
||||||
|
public final int x;
|
||||||
|
public final int y;
|
||||||
|
public final int z;
|
||||||
|
private final long[] section;
|
||||||
|
private final long populationMsk;
|
||||||
|
private final long[][] subSections;
|
||||||
|
public VoxelizedSection(long[] section, long populationMsk, long[][] subSections, int x, int y, int z) {
|
||||||
|
this.section = section;
|
||||||
|
this.populationMsk = populationMsk;
|
||||||
|
this.subSections = subSections;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getIdx(int x, int y, int z, int shiftBy, int size) {
|
||||||
|
int M = (1<<size)-1;
|
||||||
|
x = (x>>shiftBy)&M;
|
||||||
|
y = (y>>shiftBy)&M;
|
||||||
|
z = (z>>shiftBy)&M;
|
||||||
|
return (y<<(size<<1))|(z<<size)|(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long get(int lvl, int x, int y, int z) {
|
||||||
|
if (lvl < 2) {
|
||||||
|
int subIdx = getIdx(x,y,z,2-lvl,2);
|
||||||
|
var subSec = this.subSections[subIdx];
|
||||||
|
if (subSec == null) {
|
||||||
|
return Mapper.AIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lvl == 0) {
|
||||||
|
return subSec[getIdx(x,y,z,0,2)];
|
||||||
|
} else if (lvl == 1) {
|
||||||
|
return subSec[4*4*4+getIdx(x,y,z,0,1)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lvl == 2) {
|
||||||
|
return section[getIdx(x,y,z,0,2)];
|
||||||
|
} else if (lvl == 3) {
|
||||||
|
return section[4*4*4+getIdx(x,y,z,0,1)];
|
||||||
|
} else if (lvl == 4) {
|
||||||
|
return section[4*4*4+2*2*2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Mapper.UNKNOWN_MAPPING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VoxelizedSection createEmpty(int x, int y, int z) {
|
||||||
|
return new VoxelizedSection(new long[4*4*4+2*2*2+1], 0, new long[4*4*4][], x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package me.cortex.voxelmon.core.voxelization;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mipper;
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mapper;
|
||||||
|
import net.minecraft.block.BlockState;
|
||||||
|
import net.minecraft.registry.entry.RegistryEntry;
|
||||||
|
import net.minecraft.world.biome.Biome;
|
||||||
|
import net.minecraft.world.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.chunk.ReadableContainer;
|
||||||
|
|
||||||
|
public class WorldConversionFactory {
|
||||||
|
|
||||||
|
private static int I(int x, int y, int z) {
|
||||||
|
return (y<<4)|(z<<2)|x;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int J(int x, int y, int z) {
|
||||||
|
return ((y<<2)|(z<<1)|x) + 4*4*4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VoxelizedSection convert(Mapper stateMapper,
|
||||||
|
PalettedContainer<BlockState> blockContainer,
|
||||||
|
ReadableContainer<RegistryEntry<Biome>> biomeContainer,
|
||||||
|
I3dByteSupplier lightSupplier,
|
||||||
|
int sx,
|
||||||
|
int sy,
|
||||||
|
int sz) {
|
||||||
|
long[] section = new long[4*4*4+2*2*2+1];//Mipping
|
||||||
|
long[][] subSections = new long[4*4*4][];
|
||||||
|
long[] current = new long[4*4*4+2*2*2];
|
||||||
|
long msk = 0;
|
||||||
|
for (int oy = 0; oy < 4; oy++) {
|
||||||
|
for (int oz = 0; oz < 4; oz++) {
|
||||||
|
for (int ox = 0; ox < 4; ox++) {
|
||||||
|
RegistryEntry<Biome> biome = biomeContainer.get(ox, oy, oz);
|
||||||
|
int nonAir = 0;
|
||||||
|
for (int iy = 0; iy < 4; iy++) {
|
||||||
|
for (int iz = 0; iz < 4; iz++) {
|
||||||
|
for (int ix = 0; ix < 4; ix++) {
|
||||||
|
int x = (ox<<2)|ix;
|
||||||
|
int y = (oy<<2)|iy;
|
||||||
|
int z = (oz<<2)|iz;
|
||||||
|
var state = blockContainer.get(x, y, z);
|
||||||
|
if (!state.isAir()) {
|
||||||
|
nonAir++;
|
||||||
|
current[I(ix, iy, iz)] = stateMapper.getBaseId(lightSupplier.supply(x,y,z), state, biome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nonAir != 0) {
|
||||||
|
{//Generate mipping
|
||||||
|
//Mip L1
|
||||||
|
int i = 0;
|
||||||
|
for (int y = 0; y < 4; y += 2) {
|
||||||
|
for (int z = 0; z < 4; z += 2) {
|
||||||
|
for (int x = 0; x < 4; x += 2) {
|
||||||
|
current[4 * 4 * 4 + i++] = Mipper.mip(
|
||||||
|
current[I(x, y, z)], current[I(x+1, y, z)], current[I(x, y, z+1)], current[I(x+1, y, z+1)],
|
||||||
|
current[I(x, y+1, z)], current[I(x+1, y+1, z)], current[I(x, y+1, z+1)], current[I(x+1, y+1, z+1)],
|
||||||
|
stateMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Mip L2
|
||||||
|
section[I(ox, oy, oz)] = Mipper.mip(
|
||||||
|
current[J(0,0,0)], current[J(1,0,0)], current[J(0,0,1)], current[J(1,0,1)],
|
||||||
|
current[J(0,1,0)], current[J(1,1,0)], current[J(0,1,1)], current[J(1,1,1)],
|
||||||
|
stateMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update existence mask
|
||||||
|
msk |= 1L<<I(ox, oy, oz);
|
||||||
|
subSections[I(ox, oy, oz)] = current;
|
||||||
|
current = new long[4*4*4+2*2*2+1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{//Generate mipping
|
||||||
|
//Mip L3
|
||||||
|
int i = 0;
|
||||||
|
for (int y = 0; y < 4; y+=2) {
|
||||||
|
for (int z = 0; z < 4; z += 2) {
|
||||||
|
for (int x = 0; x < 4; x += 2) {
|
||||||
|
section[4 * 4 * 4 + i++] = Mipper.mip(section[I(x, y, z)], section[I(x+1, y, z)], section[I(x, y, z+1)], section[I(x+1, y, z+1)],
|
||||||
|
section[I(x, y+1, z)], section[I(x+1, y+1, z)], section[I(x, y+1, z+1)], section[I(x+1, y+1, z+1)],
|
||||||
|
stateMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Mip L4
|
||||||
|
section[4*4*4+2*2*2] = Mipper.mip(section[J(0, 0, 0)], section[J(1, 0, 0)], section[J(0, 0, 1)], section[J(1, 0, 1)],
|
||||||
|
section[J(0, 1, 0)], section[J(1, 1, 0)], section[J(0, 1, 1)], section[J(1, 1, 1)],
|
||||||
|
stateMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VoxelizedSection(section, msk, subSections, sx, sy, sz);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java
Normal file
122
src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package me.cortex.voxelmon.core.world;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ShortOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
import org.lwjgl.util.zstd.Zstd;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.lwjgl.util.zstd.Zstd.*;
|
||||||
|
|
||||||
|
public class SaveLoadSystem {
|
||||||
|
public static byte[] serialize(WorldSection section) {
|
||||||
|
var data = section.copyData();
|
||||||
|
var compressed = new Short[data.length];
|
||||||
|
Long2ShortOpenHashMap LUT = new Long2ShortOpenHashMap();
|
||||||
|
LongArrayList LUTVAL = new LongArrayList();
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
long block = data[i];
|
||||||
|
short mapping = LUT.computeIfAbsent(block, id->{
|
||||||
|
LUTVAL.add(id);
|
||||||
|
return (short)(LUTVAL.size()-1);
|
||||||
|
});
|
||||||
|
compressed[i] = mapping;
|
||||||
|
}
|
||||||
|
long[] lut = LUTVAL.toLongArray();
|
||||||
|
ByteBuffer raw = MemoryUtil.memAlloc(compressed.length*2+lut.length*8+512);
|
||||||
|
|
||||||
|
long hash = section.getKey()^(lut.length*1293481298141L);
|
||||||
|
raw.putLong(section.getKey());
|
||||||
|
raw.putInt(lut.length);
|
||||||
|
for (long id : lut) {
|
||||||
|
raw.putLong(id);
|
||||||
|
hash *= 1230987149811L;
|
||||||
|
hash += 12831;
|
||||||
|
hash ^= id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < compressed.length; i++) {
|
||||||
|
short block = compressed[i];
|
||||||
|
raw.putShort(block);
|
||||||
|
hash *= 1230987149811L;
|
||||||
|
hash += 12831;
|
||||||
|
hash ^= (block*1827631L) ^ data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
raw.putLong(hash);
|
||||||
|
|
||||||
|
raw.limit(raw.position());
|
||||||
|
raw.rewind();
|
||||||
|
ByteBuffer compressedData = MemoryUtil.memAlloc((int)ZSTD_COMPRESSBOUND(raw.remaining()));
|
||||||
|
long compressedSize = ZSTD_compress(compressedData, raw, 19);
|
||||||
|
byte[] out = new byte[(int) compressedSize];
|
||||||
|
compressedData.limit((int) compressedSize);
|
||||||
|
compressedData.get(out);
|
||||||
|
|
||||||
|
MemoryUtil.memFree(raw);
|
||||||
|
MemoryUtil.memFree(compressedData);
|
||||||
|
|
||||||
|
//Compress into a key + data pallet format
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldSection deserialize(WorldEngine world, int lvl, int x, int y, int z, byte[] data) {
|
||||||
|
var buff = MemoryUtil.memAlloc(data.length);
|
||||||
|
buff.put(data);
|
||||||
|
buff.rewind();
|
||||||
|
var decompressed = MemoryUtil.memAlloc(32*32*32*4*2);
|
||||||
|
long size = ZSTD_decompress(decompressed, buff);
|
||||||
|
MemoryUtil.memFree(buff);
|
||||||
|
decompressed.limit((int) size);
|
||||||
|
|
||||||
|
long hash = 0;
|
||||||
|
long key = decompressed.getLong();
|
||||||
|
int lutLen = decompressed.getInt();
|
||||||
|
long[] lut = new long[lutLen];
|
||||||
|
hash = key^(lut.length*1293481298141L);
|
||||||
|
for (int i = 0; i < lutLen; i++) {
|
||||||
|
lut[i] = decompressed.getLong();
|
||||||
|
hash *= 1230987149811L;
|
||||||
|
hash += 12831;
|
||||||
|
hash ^= lut[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var section = new WorldSection(lvl, x, y, z, world);
|
||||||
|
section.definitelyEmpty = false;
|
||||||
|
if (section.getKey() != key) {
|
||||||
|
throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < section.data.length; i++) {
|
||||||
|
short lutId = decompressed.getShort();
|
||||||
|
section.data[i] = lut[lutId];
|
||||||
|
hash *= 1230987149811L;
|
||||||
|
hash += 12831;
|
||||||
|
hash ^= (lutId*1827631L) ^ section.data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
long expectedHash = decompressed.getLong();
|
||||||
|
if (expectedHash != hash) {
|
||||||
|
//throw new IllegalStateException("Hash mismatch got: " + hash + " expected: " + expectedHash);
|
||||||
|
System.err.println("Hash mismatch got: " + hash + " expected: " + expectedHash + " removing region");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decompressed.hasRemaining()) {
|
||||||
|
//throw new IllegalStateException("Decompressed section had excess data");
|
||||||
|
System.err.println("Decompressed section had excess data removing region");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MemoryUtil.memFree(decompressed);
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java
Normal file
236
src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package me.cortex.voxelmon.core.world;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import me.cortex.voxelmon.core.rendering.RenderTracker;
|
||||||
|
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
|
||||||
|
import me.cortex.voxelmon.core.world.other.Mapper;
|
||||||
|
import me.cortex.voxelmon.core.world.service.SectionSavingService;
|
||||||
|
import me.cortex.voxelmon.core.world.service.VoxelIngestService;
|
||||||
|
import me.cortex.voxelmon.core.world.storage.StorageBackend;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
//Use an LMDB backend to store the world, use a local inmemory cache for lod sections
|
||||||
|
// automatically manages and invalidates sections of the world as needed
|
||||||
|
public class WorldEngine {
|
||||||
|
private static final int ACTIVE_CACHE_SIZE = 10;
|
||||||
|
|
||||||
|
public final StorageBackend storage;
|
||||||
|
private final Mapper mapper;
|
||||||
|
public final VoxelIngestService ingestService = new VoxelIngestService(this);
|
||||||
|
public final SectionSavingService savingService;
|
||||||
|
private RenderTracker renderTracker;
|
||||||
|
|
||||||
|
public void setRenderTracker(RenderTracker tracker) {
|
||||||
|
this.renderTracker = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mapper getMapper() {return this.mapper;}
|
||||||
|
|
||||||
|
private final int maxMipLevels;
|
||||||
|
|
||||||
|
//Loaded section world cache
|
||||||
|
private final Long2ObjectOpenHashMap<WorldSection>[] loadedSectionCache;
|
||||||
|
//TODO: also segment this up into an array
|
||||||
|
private final Long2ObjectOpenHashMap<AtomicReference<WorldSection>> sectionLoadingLocks = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
//What this is used for is to keep N sections acquired per layer, this stops sections from constantly being
|
||||||
|
// loaded and unloaded when accessed close together
|
||||||
|
private final ConcurrentLinkedDeque<WorldSection>[] activeSectionCache;
|
||||||
|
|
||||||
|
|
||||||
|
public WorldEngine(File storagePath, int savingServiceWorkers, int maxMipLayers) {
|
||||||
|
this.maxMipLevels = maxMipLayers;
|
||||||
|
this.loadedSectionCache = new Long2ObjectOpenHashMap[maxMipLayers];
|
||||||
|
this.activeSectionCache = new ConcurrentLinkedDeque[maxMipLayers];
|
||||||
|
for (int i = 0; i < maxMipLayers; i++) {
|
||||||
|
this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1<<(16-i));
|
||||||
|
this.activeSectionCache[i] = new ConcurrentLinkedDeque<>();
|
||||||
|
}
|
||||||
|
this.storage = new StorageBackend(storagePath);
|
||||||
|
this.mapper = new Mapper(this.storage);
|
||||||
|
this.savingService = new SectionSavingService(this, savingServiceWorkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Fixme/optimize, cause as the lvl gets higher, the size of x,y,z gets smaller so i can dynamically compact the format
|
||||||
|
// depending on the lvl, which should optimize colisions and whatnot
|
||||||
|
public static long getWorldSectionId(int lvl, int x, int y, int z) {
|
||||||
|
return ((long)lvl<<60)|((long)(y&0xFF)<<52)|((long)(z&((1<<24)-1))<<28)|((long)(x&((1<<24)-1))<<4);//NOTE: 4 bits spare for whatever
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLvl(long packed) {
|
||||||
|
return (int) (packed>>>60);
|
||||||
|
}
|
||||||
|
public static int getX(long packed) {
|
||||||
|
return (int) ((packed<<12)>>40);
|
||||||
|
}
|
||||||
|
public static int getY(long packed) {
|
||||||
|
return (int) ((packed<<4)>>56);
|
||||||
|
}
|
||||||
|
public static int getZ(long packed) {
|
||||||
|
return (int) ((packed<<4)>>40);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Try to unload the section from the world atomically, this is called from the saving service, or any release call which results in the refcount being 0
|
||||||
|
public void tryUnload(WorldSection section) {
|
||||||
|
synchronized (this.loadedSectionCache[section.lvl]) {
|
||||||
|
if (section.getRefCount() != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//TODO: make a thing where it checks if the section is dirty, if it is, enqueue it for a save first and return
|
||||||
|
|
||||||
|
section.setFreed();
|
||||||
|
var removedSection = this.loadedSectionCache[section.lvl].remove(section.getKey());
|
||||||
|
if (removedSection != section) {
|
||||||
|
throw new IllegalStateException("Removed section not the same as attempted to remove");
|
||||||
|
}
|
||||||
|
if (section.isAcquired()) {
|
||||||
|
throw new IllegalStateException("Section that was just removed got reacquired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Internal helper method for getOrLoad to segment up code
|
||||||
|
private WorldSection unsafeLoadSection(long key, int lvl, int x, int y, int z) {
|
||||||
|
var data = this.storage.getSectionData(key);
|
||||||
|
if (data == null) {
|
||||||
|
return new WorldSection(lvl, x, y, z, this);
|
||||||
|
} else {
|
||||||
|
var ret = SaveLoadSystem.deserialize(this, lvl, x, y, z, data);
|
||||||
|
if (ret != null) {
|
||||||
|
return ret;
|
||||||
|
} else {
|
||||||
|
this.storage.deleteSectionData(key);
|
||||||
|
return new WorldSection(lvl, x, y, z, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Gets a loaded section or loads the section from storage
|
||||||
|
public WorldSection getOrLoadAcquire(int lvl, int x, int y, int z) {
|
||||||
|
long key = getWorldSectionId(lvl, x, y, z);
|
||||||
|
|
||||||
|
AtomicReference<WorldSection> lock = null;
|
||||||
|
AtomicReference<WorldSection> gotLock = null;
|
||||||
|
synchronized (this.loadedSectionCache[lvl]) {
|
||||||
|
var result = this.loadedSectionCache[lvl].get(key);
|
||||||
|
if (result != null) {
|
||||||
|
result.acquire();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
lock = new AtomicReference<>(null);
|
||||||
|
synchronized (this.sectionLoadingLocks) {
|
||||||
|
var finalLock = lock;
|
||||||
|
gotLock = this.sectionLoadingLocks.computeIfAbsent(key, a -> finalLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//We acquired the lock so load it
|
||||||
|
if (gotLock == lock) {
|
||||||
|
WorldSection loadedSection = this.unsafeLoadSection(key, lvl, x, y, z);
|
||||||
|
loadedSection.acquire();
|
||||||
|
|
||||||
|
|
||||||
|
//Insert the loaded section and set the loading lock to the loaded value
|
||||||
|
synchronized (this.loadedSectionCache[lvl]) {
|
||||||
|
this.loadedSectionCache[lvl].put(key, loadedSection);
|
||||||
|
synchronized (this.sectionLoadingLocks) {
|
||||||
|
this.sectionLoadingLocks.remove(key);
|
||||||
|
lock.set(loadedSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add to the active acquired cache and remove the last item if the size is over the limit
|
||||||
|
{
|
||||||
|
loadedSection.acquire();
|
||||||
|
this.activeSectionCache[lvl].add(loadedSection);
|
||||||
|
if (this.activeSectionCache[lvl].size() > ACTIVE_CACHE_SIZE) {
|
||||||
|
var last = this.activeSectionCache[lvl].pop();
|
||||||
|
last.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedSection;
|
||||||
|
} else {
|
||||||
|
lock = gotLock;
|
||||||
|
//Another thread got the lock so spin wait for the section to load
|
||||||
|
while (lock.get() == null) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
var section = lock.get();
|
||||||
|
//Fixme: try find a better solution for this
|
||||||
|
|
||||||
|
//The issue with this is that the section could be unloaded when we acquire it cause of so many threading pain
|
||||||
|
// so lock the section cache, try acquire the section, if we fail we must load the section again
|
||||||
|
synchronized (this.loadedSectionCache[lvl]) {
|
||||||
|
if (section.tryAcquire()) {
|
||||||
|
//We acquired the section successfully, return it
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//We failed to acquire the section, we must reload it
|
||||||
|
return this.getOrLoadAcquire(lvl, x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Marks a section as dirty, enqueuing it for saving and or render data rebuilding
|
||||||
|
private void markDirty(WorldSection section) {
|
||||||
|
this.renderTracker.sectionUpdated(section);
|
||||||
|
//TODO: add an option for having synced saving, that is when call enqueueSave, that will instead, instantly
|
||||||
|
// save to the db, this can be useful for just reducing the amount of thread pools in total
|
||||||
|
// might have some issues with threading if the same section is saved from multiple threads?
|
||||||
|
this.savingService.enqueueSave(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Executes an update to the world and automatically updates all the parent mip layers up to level 4 (e.g. where 1 chunk section is 1 block big)
|
||||||
|
public void insertUpdate(VoxelizedSection section) {
|
||||||
|
//The >>1 is cause the world sections size is 32x32x32 vs the 16x16x16 of the voxelized section
|
||||||
|
for (int lvl = 0; lvl < this.maxMipLevels; lvl++) {
|
||||||
|
var worldSection = this.getOrLoadAcquire(lvl, section.x>>(lvl+1), section.y>>(lvl+1), section.z>>(lvl+1));
|
||||||
|
int msk = (1<<(lvl+1))-1;
|
||||||
|
int bx = (section.x&msk)<<(4-lvl);
|
||||||
|
int by = (section.y&msk)<<(4-lvl);
|
||||||
|
int bz = (section.z&msk)<<(4-lvl);
|
||||||
|
boolean didChange = false;
|
||||||
|
for (int y = by; y < (16>>lvl)+by; y++) {
|
||||||
|
for (int z = bz; z < (16>>lvl)+bz; z++) {
|
||||||
|
for (int x = bx; x < (16>>lvl)+bx; x++) {
|
||||||
|
long newId = section.get(lvl, x-bx, y-by, z-bz);
|
||||||
|
long oldId = worldSection.set(x, y, z, newId);
|
||||||
|
didChange |= newId != oldId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to release the section after using it
|
||||||
|
if (didChange) {
|
||||||
|
//Mark the section as dirty (enqueuing saving and geometry rebuild) and move to parent mip level
|
||||||
|
this.markDirty(worldSection);
|
||||||
|
worldSection.release();
|
||||||
|
} else {
|
||||||
|
//If nothing changed just need to release, dont need to update parent mips
|
||||||
|
worldSection.release();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getLoadedSectionCacheSizes() {
|
||||||
|
var res = new int[this.maxMipLevels];
|
||||||
|
for (int i = 0; i < this.maxMipLevels; i++) {
|
||||||
|
res[i] = this.loadedSectionCache[i].size();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
try {this.storage.flush();} catch (Exception e) {System.err.println(e);}
|
||||||
|
//Shutdown in this order to preserve as much data as possible
|
||||||
|
try {this.ingestService.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||||
|
try {this.savingService.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||||
|
try {this.storage.close();} catch (Exception e) {System.err.println(e);}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/main/java/me/cortex/voxelmon/core/world/WorldSection.java
Normal file
125
src/main/java/me/cortex/voxelmon/core/world/WorldSection.java
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package me.cortex.voxelmon.core.world;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
//Represents a loaded world section at a specific detail level
|
||||||
|
// holds a 32x32x32 region of detail
|
||||||
|
public final class WorldSection {
|
||||||
|
public final int lvl;
|
||||||
|
public final int x;
|
||||||
|
public final int y;
|
||||||
|
public final int z;
|
||||||
|
|
||||||
|
////Maps from a local id to global meaning it should be much cheaper to store in memory probably
|
||||||
|
//private final int[] dataMapping = null;
|
||||||
|
//private final short[] data = new short[32*32*32];
|
||||||
|
final long[] data = new long[32*32*32];
|
||||||
|
boolean definitelyEmpty = true;
|
||||||
|
|
||||||
|
private final WorldEngine world;
|
||||||
|
|
||||||
|
public WorldSection(int lvl, int x, int y, int z, WorldEngine worldIn) {
|
||||||
|
this.lvl = lvl;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.world = worldIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return ((x*1235641+y)*8127451+z)*918267913+lvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final AtomicBoolean inSaveQueue = new AtomicBoolean();
|
||||||
|
private final AtomicInteger usageCounts = new AtomicInteger();
|
||||||
|
|
||||||
|
public int acquire() {
|
||||||
|
this.assertNotFree();
|
||||||
|
return this.usageCounts.getAndAdd(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Fixme i dont think this is fully thread safe/correct
|
||||||
|
public boolean tryAcquire() {
|
||||||
|
if (this.freed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.usageCounts.getAndAdd(1);
|
||||||
|
if (this.freed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int release() {
|
||||||
|
this.assertNotFree();
|
||||||
|
int i = this.usageCounts.addAndGet(-1);
|
||||||
|
if (i < 0) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//NOTE: cant actually check for not free as at this stage it technically could be unloaded, as soon
|
||||||
|
//this.assertNotFree();
|
||||||
|
|
||||||
|
|
||||||
|
//Try to unload the section if its empty
|
||||||
|
if (i == 0) {
|
||||||
|
this.world.tryUnload(this);
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private volatile boolean freed = false;
|
||||||
|
void setFreed() {
|
||||||
|
this.assertNotFree();
|
||||||
|
this.freed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertNotFree() {
|
||||||
|
if (this.freed) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAcquired() {
|
||||||
|
return this.usageCounts.get() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRefCount() {
|
||||||
|
return this.usageCounts.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getKey() {
|
||||||
|
return WorldEngine.getWorldSectionId(this.lvl, this.x, this.y, this.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getIndex(int x, int y, int z) {
|
||||||
|
int M = (1<<5)-1;
|
||||||
|
if (x<0||x>M||y<0||y>M||z<0||z>M) {
|
||||||
|
throw new IllegalArgumentException("Out of bounds: " + x + ", " + y + ", " + z);
|
||||||
|
}
|
||||||
|
return ((y&M)<<10)|((z&M)<<5)|(x&M);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long set(int x, int y, int z, long id) {
|
||||||
|
int idx = getIndex(x,y,z);
|
||||||
|
long old = this.data[idx];
|
||||||
|
this.data[idx] = id;
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generates a copy of the data array, this is to help with atomic operations like rendering
|
||||||
|
public long[] copyData() {
|
||||||
|
return Arrays.copyOf(this.data, this.data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean definitelyEmpty() {
|
||||||
|
return this.definitelyEmpty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: for serialization, make a huffman encoding tree on the integers since that should be very very efficent for compression
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.other;
|
||||||
|
|
||||||
|
public record BiomeColour(int id, int foliageColour, int waterColour) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.other;
|
||||||
|
|
||||||
|
public record BlockStateColour(int id, int biomeTintMsk, int[] faceColours) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.other;
|
||||||
|
|
||||||
|
import net.minecraft.block.BlockState;
|
||||||
|
import net.minecraft.block.Blocks;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.render.model.BakedQuad;
|
||||||
|
import net.minecraft.client.texture.NativeImage;
|
||||||
|
import net.minecraft.client.texture.Sprite;
|
||||||
|
import net.minecraft.registry.RegistryKeys;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
import net.minecraft.util.math.random.LocalRandom;
|
||||||
|
import net.minecraft.world.biome.BiomeKeys;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ColourResolver {
|
||||||
|
//TODO: sample from multiple random values and avg it
|
||||||
|
public static int[] resolveColour(BlockState state) {
|
||||||
|
return resolveColour(state, 1234567890L);
|
||||||
|
}
|
||||||
|
|
||||||
|
//The way this works is it takes the and computes its colour, it then computes the area of the quad and the normal direction
|
||||||
|
// it adds each area and colour to a per direcition colour
|
||||||
|
// for non specific axis dimensions it takes the normal of each quad computes the dot between it and each of the directions
|
||||||
|
// and averages that
|
||||||
|
// if the colour doesnt exist for a specific axis set it to the average of the other axis and or make it translucent
|
||||||
|
|
||||||
|
//TODO: fixme: finish
|
||||||
|
public static int[] resolveColour(BlockState state, long randomValue) {
|
||||||
|
if (state == Blocks.AIR.getDefaultState()) {
|
||||||
|
return new int[6];
|
||||||
|
}
|
||||||
|
int[][] builder = new int[6][5];
|
||||||
|
var random = new LocalRandom(randomValue);
|
||||||
|
if (state.getFluidState().isEmpty()) {
|
||||||
|
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
|
||||||
|
var quads = MinecraftClient.getInstance()
|
||||||
|
.getBakedModelManager()
|
||||||
|
.getBlockModels()
|
||||||
|
.getModel(state)
|
||||||
|
.getQuads(state, direction, random);
|
||||||
|
for (var quad : quads) {
|
||||||
|
long weightColour = resolveQuadColour(quad);
|
||||||
|
int colour = (int) weightColour;
|
||||||
|
int weight = (int) (weightColour>>32);
|
||||||
|
if (direction == null) {
|
||||||
|
//TODO: apply normal multiplication to weight
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
builder[i][0] += weight;
|
||||||
|
builder[i][4] += weight * ((colour>>>24)&0xFF);
|
||||||
|
builder[i][3] += weight * ((colour>>>16)&0xFF);
|
||||||
|
builder[i][2] += weight * ((colour>>>8)&0xFF);
|
||||||
|
builder[i][1] += weight * ((colour>>>0)&0xFF);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder[direction.getId()][0] += weight;
|
||||||
|
builder[direction.getId()][4] += weight*((colour>>>24)&0xFF);
|
||||||
|
builder[direction.getId()][3] += weight*((colour>>>16)&0xFF);
|
||||||
|
builder[direction.getId()][2] += weight*((colour>>>8)&0xFF);
|
||||||
|
builder[direction.getId()][1] += weight*((colour>>>0)&0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//TODO FIXME: need to account for both the fluid and block state at the same time
|
||||||
|
//FIXME: make it not hacky and use the fluid handler thing from fabric
|
||||||
|
|
||||||
|
long weightColour = resolveNI(MinecraftClient.getInstance().getBakedModelManager().getBlockModels().getModelParticleSprite(state).getContents().image);
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
builder[i][0] = 1;
|
||||||
|
builder[i][1] += (weightColour>>0)&0xFF;
|
||||||
|
builder[i][2] += (weightColour>>8)&0xFF;
|
||||||
|
builder[i][3] += (weightColour>>16)&0xFF;
|
||||||
|
builder[i][4] += (weightColour>>24)&0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] out = new int[6];
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
int c = builder[i][0];
|
||||||
|
if (c == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int r = builder[i][4]/c;
|
||||||
|
int g = builder[i][3]/c;
|
||||||
|
int b = builder[i][2]/c;
|
||||||
|
int a = builder[i][1]/c;
|
||||||
|
out[i] = (r<<24)|(g<<16)|(b<<8)|a;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long resolveQuadColour(BakedQuad quad) {
|
||||||
|
return resolveNI(quad.getSprite().getContents().image);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long resolveNI(NativeImage image) {
|
||||||
|
int r = 0;
|
||||||
|
int g = 0;
|
||||||
|
int b = 0;
|
||||||
|
int a = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
int colour = image.getColor(x, y);
|
||||||
|
if (((colour >>> 24)&0xFF) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r += (colour >>> 0) & 0xFF;
|
||||||
|
g += (colour >>> 8) & 0xFF;
|
||||||
|
b += (colour >>> 16) & 0xFF;
|
||||||
|
a += (colour >>> 24) & 0xFF;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
r /= count;
|
||||||
|
g /= count;
|
||||||
|
b /= count;
|
||||||
|
a /= count;
|
||||||
|
|
||||||
|
int colour = (r<<24)|(g<<16)|(b<<8)|a;
|
||||||
|
|
||||||
|
return Integer.toUnsignedLong(colour)|(((long)count)<<32);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static long resolveBiomeColour(String biomeId) {
|
||||||
|
var biome = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME).get(new Identifier(biomeId));
|
||||||
|
int ARGBFoliage = biome.getFoliageColor();
|
||||||
|
int ARGBWater = biome.getWaterColor();
|
||||||
|
return Integer.toUnsignedLong(((ARGBFoliage&0xFFFFFF)<<8)|(ARGBFoliage>>>24)) | (Integer.toUnsignedLong(((ARGBWater&0xFFFFFF)<<8)|(ARGBWater>>>24))<<32);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/main/java/me/cortex/voxelmon/core/world/other/Mapper.java
Normal file
237
src/main/java/me/cortex/voxelmon/core/world/other/Mapper.java
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.other;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||||
|
import me.cortex.voxelmon.core.util.MemoryBuffer;
|
||||||
|
import me.cortex.voxelmon.core.world.storage.StorageBackend;
|
||||||
|
import net.minecraft.block.BlockState;
|
||||||
|
import net.minecraft.block.Blocks;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.nbt.NbtIo;
|
||||||
|
import net.minecraft.nbt.NbtOps;
|
||||||
|
import net.minecraft.registry.entry.RegistryEntry;
|
||||||
|
import net.minecraft.world.biome.Biome;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
//There are independent mappings for biome and block states, these get combined in the shader and allow for more
|
||||||
|
// variaty of things
|
||||||
|
public class Mapper {
|
||||||
|
private static final int BLOCK_STATE_TYPE = 1;
|
||||||
|
private static final int BIOME_TYPE = 2;
|
||||||
|
|
||||||
|
private final StorageBackend storage;
|
||||||
|
public static final int UNKNOWN_MAPPING = -1;
|
||||||
|
public static final int AIR = 0;
|
||||||
|
|
||||||
|
private final Object2ObjectOpenHashMap<BlockState, StateEntry> block2stateEntry = new Object2ObjectOpenHashMap<>();
|
||||||
|
private final ObjectArrayList<StateEntry> blockId2stateEntry = new ObjectArrayList<>();
|
||||||
|
private final Object2ObjectOpenHashMap<String, BiomeEntry> biome2biomeEntry = new Object2ObjectOpenHashMap<>();
|
||||||
|
private final ObjectArrayList<BiomeEntry> biomeId2biomeEntry = new ObjectArrayList<>();
|
||||||
|
public Mapper(StorageBackend storage) {
|
||||||
|
this.storage = storage;
|
||||||
|
//Insert air since its a special entry (index 0)
|
||||||
|
var airEntry = new StateEntry(0, Blocks.AIR.getDefaultState());
|
||||||
|
block2stateEntry.put(airEntry.state, airEntry);
|
||||||
|
blockId2stateEntry.add(airEntry);
|
||||||
|
|
||||||
|
this.loadFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFromStorage() {
|
||||||
|
var mappings = this.storage.getIdMappings();
|
||||||
|
List<StateEntry> sentries = new ArrayList<>();
|
||||||
|
List<BiomeEntry> bentries = new ArrayList<>();
|
||||||
|
for (var entry : mappings.int2ObjectEntrySet()) {
|
||||||
|
int entryType = entry.getIntKey()>>>30;
|
||||||
|
int id = entry.getIntKey() & ((1<<30)-1);
|
||||||
|
if (entryType == BLOCK_STATE_TYPE) {
|
||||||
|
var sentry = StateEntry.deserialize(id, entry.getValue());
|
||||||
|
sentries.add(sentry);
|
||||||
|
if (this.block2stateEntry.put(sentry.state, sentry) != null) {
|
||||||
|
throw new IllegalStateException("Multiple mappings for blockstate");
|
||||||
|
}
|
||||||
|
} else if (entryType == BIOME_TYPE) {
|
||||||
|
var bentry = BiomeEntry.deserialize(id, entry.getValue());
|
||||||
|
bentries.add(bentry);
|
||||||
|
if (this.biome2biomeEntry.put(bentry.biome, bentry) != null) {
|
||||||
|
throw new IllegalStateException("Multiple mappings for biome entry");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unknown entryType");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Insert into the arrays
|
||||||
|
sentries.stream().sorted(Comparator.comparing(a->a.id)).forEach(entry -> {
|
||||||
|
if (this.blockId2stateEntry.size() != entry.id) {
|
||||||
|
throw new IllegalStateException("Block entry not ordered");
|
||||||
|
}
|
||||||
|
this.blockId2stateEntry.add(entry);
|
||||||
|
});
|
||||||
|
bentries.stream().sorted(Comparator.comparing(a->a.id)).forEach(entry -> {
|
||||||
|
if (this.biomeId2biomeEntry.size() != entry.id) {
|
||||||
|
throw new IllegalStateException("Biome entry not ordered");
|
||||||
|
}
|
||||||
|
this.biomeId2biomeEntry.add(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private StateEntry registerNewBlockState(BlockState state) {
|
||||||
|
StateEntry entry = new StateEntry(this.blockId2stateEntry.size(), state);
|
||||||
|
this.block2stateEntry.put(state, entry);
|
||||||
|
this.blockId2stateEntry.add(entry);
|
||||||
|
|
||||||
|
byte[] serialized = entry.serialize();
|
||||||
|
ByteBuffer buffer = MemoryUtil.memAlloc(serialized.length);
|
||||||
|
buffer.put(serialized);
|
||||||
|
buffer.rewind();
|
||||||
|
this.storage.putIdMapping(entry.id | (BLOCK_STATE_TYPE<<30), buffer);
|
||||||
|
MemoryUtil.memFree(buffer);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeEntry registerNewBiome(String biome) {
|
||||||
|
BiomeEntry entry = new BiomeEntry(this.biome2biomeEntry.size(), biome);
|
||||||
|
this.biome2biomeEntry.put(biome, entry);
|
||||||
|
this.biomeId2biomeEntry.add(entry);
|
||||||
|
|
||||||
|
byte[] serialized = entry.serialize();
|
||||||
|
ByteBuffer buffer = MemoryUtil.memAlloc(serialized.length);
|
||||||
|
buffer.put(serialized);
|
||||||
|
buffer.rewind();
|
||||||
|
this.storage.putIdMapping(entry.id | (BIOME_TYPE<<30), buffer);
|
||||||
|
MemoryUtil.memFree(buffer);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO:FIXME: IS VERY SLOW NEED TO MAKE IT LOCK FREE
|
||||||
|
public long getBaseId(byte light, BlockState state, RegistryEntry<Biome> biome) {
|
||||||
|
StateEntry sentry = null;
|
||||||
|
BiomeEntry bentry = null;
|
||||||
|
synchronized (this.block2stateEntry) {
|
||||||
|
sentry = this.block2stateEntry.get(state);
|
||||||
|
if (sentry == null) {
|
||||||
|
sentry = this.registerNewBlockState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized (this.biome2biomeEntry) {
|
||||||
|
String biomeId = biome.getKey().get().getValue().toString();
|
||||||
|
bentry = this.biome2biomeEntry.get(biomeId);
|
||||||
|
if (bentry == null) {
|
||||||
|
bentry = this.registerNewBiome(biomeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Byte.toUnsignedLong(light)<<56)|(Integer.toUnsignedLong(bentry.id) << 47)|(Integer.toUnsignedLong(sentry.id)<<27);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockState[] getBlockStates() {
|
||||||
|
synchronized (this.block2stateEntry) {
|
||||||
|
BlockState[] out = new BlockState[this.blockId2stateEntry.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (var entry : this.blockId2stateEntry) {
|
||||||
|
if (entry.id != i++) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
out[i-1] = entry.state;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getBiomes() {
|
||||||
|
synchronized (this.biome2biomeEntry) {
|
||||||
|
String[] out = new String[this.biome2biomeEntry.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (var entry : this.biomeId2biomeEntry) {
|
||||||
|
if (entry.id != i++) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
out[i-1] = entry.biome;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class StateEntry {
|
||||||
|
private final int id;
|
||||||
|
private final BlockState state;
|
||||||
|
public StateEntry(int id, BlockState state) {
|
||||||
|
this.id = id;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize() {
|
||||||
|
try {
|
||||||
|
var serialized = new NbtCompound();
|
||||||
|
serialized.putInt("id", this.id);
|
||||||
|
serialized.put("block_state", BlockState.CODEC.encodeStart(NbtOps.INSTANCE, this.state).result().get());
|
||||||
|
var out = new ByteArrayOutputStream();
|
||||||
|
NbtIo.writeCompressed(serialized, out);
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StateEntry deserialize(int id, byte[] data) {
|
||||||
|
try {
|
||||||
|
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data));
|
||||||
|
if (compound.getInt("id") != id) {
|
||||||
|
throw new IllegalStateException("Encoded id != expected id");
|
||||||
|
}
|
||||||
|
BlockState state = BlockState.CODEC.parse(NbtOps.INSTANCE, compound.getCompound("block_state")).get().orThrow();
|
||||||
|
return new StateEntry(id, state);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class BiomeEntry {
|
||||||
|
private final int id;
|
||||||
|
private final String biome;
|
||||||
|
|
||||||
|
public BiomeEntry(int id, String biome) {
|
||||||
|
this.id = id;
|
||||||
|
this.biome = biome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize() {
|
||||||
|
try {
|
||||||
|
var serialized = new NbtCompound();
|
||||||
|
serialized.putInt("id", this.id);
|
||||||
|
serialized.putString("biome_id", this.biome);
|
||||||
|
var out = new ByteArrayOutputStream();
|
||||||
|
NbtIo.writeCompressed(serialized, out);
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BiomeEntry deserialize(int id, byte[] data) {
|
||||||
|
try {
|
||||||
|
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data));
|
||||||
|
if (compound.getInt("id") != id) {
|
||||||
|
throw new IllegalStateException("Encoded id != expected id");
|
||||||
|
}
|
||||||
|
String biome = compound.getString("biome_id");
|
||||||
|
return new BiomeEntry(id, biome);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.other;
|
||||||
|
|
||||||
|
//Mipper for data
|
||||||
|
public class Mipper {
|
||||||
|
//TODO: also pass in the level its mipping from, cause at lower levels you want to preserve block details
|
||||||
|
// but at higher details you want more air
|
||||||
|
public static long mip(long I000, long I100, long I001, long I101,
|
||||||
|
long I010, long I110, long I011, long I111,
|
||||||
|
Mapper mapper) {
|
||||||
|
//TODO: mip with respect to all the variables, what that means is take whatever has the highest count and return that
|
||||||
|
//TODO: also average out the light level and set that as the new light level
|
||||||
|
//For now just take the most top corner
|
||||||
|
if (I111 != 0) {
|
||||||
|
return I111;
|
||||||
|
}
|
||||||
|
if (I110 != 0) {
|
||||||
|
return I110;
|
||||||
|
}
|
||||||
|
if (I011 != 0) {
|
||||||
|
return I011;
|
||||||
|
}
|
||||||
|
if (I010 != 0) {
|
||||||
|
return I010;
|
||||||
|
}
|
||||||
|
if (I101 != 0) {
|
||||||
|
return I101;
|
||||||
|
}
|
||||||
|
if (I100 != 0) {
|
||||||
|
return I100;
|
||||||
|
}
|
||||||
|
if (I001 != 0) {
|
||||||
|
return I001;
|
||||||
|
}
|
||||||
|
if (I000 != 0) {
|
||||||
|
return I000;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.service;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.world.SaveLoadSystem;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldSection;
|
||||||
|
import net.minecraft.world.chunk.WorldChunk;
|
||||||
|
import org.lwjgl.system.MemoryStack;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
|
//TODO: add an option for having synced saving, that is when call enqueueSave, that will instead, instantly
|
||||||
|
// save to the db, this can be useful for just reducing the amount of thread pools in total
|
||||||
|
// might have some issues with threading if the same section is saved from multiple threads?
|
||||||
|
public class SectionSavingService {
|
||||||
|
private volatile boolean running = true;
|
||||||
|
private final Thread[] workers;
|
||||||
|
|
||||||
|
private final ConcurrentLinkedDeque<WorldSection> saveQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
private final Semaphore saveCounter = new Semaphore(0);
|
||||||
|
|
||||||
|
private final WorldEngine world;
|
||||||
|
|
||||||
|
public SectionSavingService(WorldEngine worldEngine, int workers) {
|
||||||
|
this.workers = new Thread[workers];
|
||||||
|
for (int i = 0; i < workers; i++) {
|
||||||
|
var worker = new Thread(this::saveWorker);
|
||||||
|
worker.setDaemon(false);
|
||||||
|
worker.setName("Saving service #" + i);
|
||||||
|
worker.start();
|
||||||
|
this.workers[i] = worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.world = worldEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveWorker() {
|
||||||
|
while (running) {
|
||||||
|
this.saveCounter.acquireUninterruptibly();
|
||||||
|
if (!this.running) break;
|
||||||
|
var section = this.saveQueue.pop();
|
||||||
|
section.assertNotFree();
|
||||||
|
section.inSaveQueue.set(false);
|
||||||
|
|
||||||
|
//TODO: stop converting between all these types and just use a native buffer all the time
|
||||||
|
var saveData = SaveLoadSystem.serialize(section);
|
||||||
|
//Note: this is done like this because else the gc can collect the buffer before the transaction is completed
|
||||||
|
// thus the transaction reads from undefined memory
|
||||||
|
ByteBuffer buffer = MemoryUtil.memAlloc(saveData.length);
|
||||||
|
buffer.put(saveData).rewind();
|
||||||
|
this.world.storage.setSectionData(section.getKey(), buffer);
|
||||||
|
MemoryUtil.memFree(buffer);
|
||||||
|
|
||||||
|
section.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueSave(WorldSection section) {
|
||||||
|
//If its not enqueued for saving then enqueue it
|
||||||
|
if (!section.inSaveQueue.getAndSet(true)) {
|
||||||
|
//Acquire the section for use
|
||||||
|
section.acquire();
|
||||||
|
this.saveQueue.add(section);
|
||||||
|
this.saveCounter.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
boolean anyAlive = false;
|
||||||
|
for (var worker : this.workers) {
|
||||||
|
anyAlive |= worker.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyAlive) {
|
||||||
|
System.err.println("Section saving workers already dead on shutdown! this is very very bad, check log for errors from this thread");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Wait for all the saving to finish
|
||||||
|
while (this.saveCounter.availablePermits() != 0) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
//Shutdown
|
||||||
|
this.running = false;
|
||||||
|
this.saveCounter.release(1000);
|
||||||
|
//Wait for threads to join
|
||||||
|
try {
|
||||||
|
for (var worker : this.workers) {
|
||||||
|
worker.join();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {throw new RuntimeException(e);}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTaskCount() {
|
||||||
|
return this.saveCounter.availablePermits();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.service;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
|
||||||
|
import me.cortex.voxelmon.core.voxelization.WorldConversionFactory;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import net.minecraft.world.chunk.WorldChunk;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
|
public class VoxelIngestService {
|
||||||
|
private volatile boolean running = true;
|
||||||
|
private final Thread worker;
|
||||||
|
|
||||||
|
private final ConcurrentLinkedDeque<WorldChunk> ingestQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
private final Semaphore ingestCounter = new Semaphore(0);
|
||||||
|
|
||||||
|
private final WorldEngine world;
|
||||||
|
public VoxelIngestService(WorldEngine world) {
|
||||||
|
this.worker = new Thread(this::ingestWorker);
|
||||||
|
this.worker.setDaemon(false);
|
||||||
|
this.worker.setName("Ingest service");
|
||||||
|
this.worker.start();
|
||||||
|
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ingestWorker() {
|
||||||
|
while (this.running) {
|
||||||
|
this.ingestCounter.acquireUninterruptibly();
|
||||||
|
if (!this.running) break;
|
||||||
|
var chunk = this.ingestQueue.pop();
|
||||||
|
int i = chunk.getBottomSectionCoord() - 1;
|
||||||
|
for (var section : chunk.getSectionArray()) {
|
||||||
|
i++;
|
||||||
|
if (section.isEmpty()) {
|
||||||
|
this.world.insertUpdate(VoxelizedSection.createEmpty(chunk.getPos().x, i, chunk.getPos().z));
|
||||||
|
} else {
|
||||||
|
VoxelizedSection csec = WorldConversionFactory.convert(
|
||||||
|
this.world.getMapper(),
|
||||||
|
section.getBlockStateContainer(),
|
||||||
|
section.getBiomeContainer(),
|
||||||
|
(x, y, z) -> (byte) 0,
|
||||||
|
chunk.getPos().x,
|
||||||
|
i,
|
||||||
|
chunk.getPos().z
|
||||||
|
);
|
||||||
|
|
||||||
|
this.world.insertUpdate(csec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enqueueIngest(WorldChunk chunk) {
|
||||||
|
this.ingestQueue.add(chunk);
|
||||||
|
this.ingestCounter.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTaskCount() {
|
||||||
|
return this.ingestCounter.availablePermits();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (!this.worker.isAlive()) {
|
||||||
|
System.err.println("Ingest worker already dead on shutdown! this is very very bad, check log for errors from this thread");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Wait for the ingest to finish
|
||||||
|
while (this.ingestCounter.availablePermits() != 0) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
//Shutdown
|
||||||
|
this.running = false;
|
||||||
|
this.ingestCounter.release(1000);
|
||||||
|
//Wait for thread to join
|
||||||
|
try {this.worker.join();} catch (InterruptedException e) {throw new RuntimeException(e);}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
import org.lwjgl.util.lmdb.MDBVal;
|
||||||
|
|
||||||
|
import static me.cortex.voxelmon.core.world.storage.LMDBInterface.E;
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.*;
|
||||||
|
|
||||||
|
public class Cursor implements AutoCloseable {
|
||||||
|
private final long cursor;
|
||||||
|
public Cursor(long cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(int op, MDBVal key, MDBVal data) {
|
||||||
|
int e = mdb_cursor_get(this.cursor, key, data, op);
|
||||||
|
if (e != MDB_SUCCESS && e != MDB_NOTFOUND) {
|
||||||
|
E(e);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mdb_cursor_close(this.cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
import org.lwjgl.PointerBuffer;
|
||||||
|
import org.lwjgl.system.MemoryStack;
|
||||||
|
import org.lwjgl.util.lmdb.LMDB;
|
||||||
|
import org.lwjgl.util.lmdb.MDBEnvInfo;
|
||||||
|
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
import static org.lwjgl.system.MemoryStack.stackPush;
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.*;
|
||||||
|
|
||||||
|
public class LMDBInterface {
|
||||||
|
private final long env;
|
||||||
|
private LMDBInterface(long env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final long env;
|
||||||
|
public Builder() {
|
||||||
|
//Create the environment
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
PointerBuffer pp = stack.mallocPointer(1);
|
||||||
|
E(mdb_env_create(pp));
|
||||||
|
this.env = pp.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setMaxDbs(int maxDbs) {
|
||||||
|
E(mdb_env_set_maxdbs(this.env, maxDbs));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder open(String directory, int flags) {
|
||||||
|
E(mdb_env_open(this.env, directory, flags, 0664));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LMDBInterface fetch() {
|
||||||
|
return new LMDBInterface(this.env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
mdb_env_close(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void E(int rc) {
|
||||||
|
if (rc != MDB_SUCCESS) {
|
||||||
|
throw new IllegalStateException("Code: " + rc + " msg: " + mdb_strerror(rc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMapSize(long size) {
|
||||||
|
E(mdb_env_set_mapsize(this.env, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T transaction(TransactionCallback<T> transaction) {
|
||||||
|
return transaction(0, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T transaction(int flags, TransactionCallback<T> transaction) {
|
||||||
|
return transaction(0, flags, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T transaction(long parent, int flags, TransactionCallback<T> transaction) {
|
||||||
|
T ret;
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
PointerBuffer pp = stack.mallocPointer(1);
|
||||||
|
E(mdb_txn_begin(this.env, parent, flags, pp));
|
||||||
|
long txn = pp.get(0);
|
||||||
|
int err;
|
||||||
|
try {
|
||||||
|
ret = transaction.exec(stack, txn);
|
||||||
|
err = mdb_txn_commit(txn);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
mdb_txn_abort(txn);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
E(err);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Database createDb(String name) {
|
||||||
|
return this.createDb(name, MDB_CREATE|MDB_INTEGERKEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Database createDb(String name, int flags) {
|
||||||
|
return new Database(name, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LMDBInterface flush(boolean force) {
|
||||||
|
E(mdb_env_sync(this.env, force));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMapSize() {
|
||||||
|
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||||
|
MDBEnvInfo info = MDBEnvInfo.calloc(stack);
|
||||||
|
E(mdb_env_info(this.env, info));
|
||||||
|
return info.me_mapsize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Database {
|
||||||
|
private final int dbi;
|
||||||
|
public Database(String name, int flags) {
|
||||||
|
this.dbi = LMDBInterface.this.transaction((stack, txn)-> {
|
||||||
|
IntBuffer ip = stack.mallocInt(1);
|
||||||
|
E(mdb_dbi_open(txn, name, flags, ip));
|
||||||
|
return ip.get(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
mdb_dbi_close(LMDBInterface.this.env, this.dbi);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: make a MDB_RDONLY varient
|
||||||
|
public <T> T transaction(TransactionWrappedCallback<T> callback) {
|
||||||
|
return this.transaction(0, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T transaction(int flags, TransactionWrappedCallback<T> callback) {
|
||||||
|
return LMDBInterface.this.transaction(flags, (stack, transaction) -> {
|
||||||
|
return callback.exec(new TransactionWrapper(transaction, stack).set(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDBI() {
|
||||||
|
return this.dbi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
|
import org.lwjgl.util.lmdb.MDBVal;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.*;
|
||||||
|
|
||||||
|
public class StorageBackend {
|
||||||
|
private static final long GROW_SIZE = 1<<25;//Grow by 33 mb each time
|
||||||
|
|
||||||
|
private final AtomicInteger accessingCounts = new AtomicInteger();
|
||||||
|
private final ReentrantLock resizeLock = new ReentrantLock();
|
||||||
|
|
||||||
|
private final LMDBInterface dbi;
|
||||||
|
private final LMDBInterface.Database sectionDatabase;
|
||||||
|
private final LMDBInterface.Database idMappingDatabase;
|
||||||
|
public StorageBackend(File file) {
|
||||||
|
this.dbi = new LMDBInterface.Builder()
|
||||||
|
.setMaxDbs(2)
|
||||||
|
.open(file.getAbsolutePath(), MDB_NOSUBDIR)//MDB_NOLOCK (IF I DO THIS, must sync the db manually)// TODO: THIS
|
||||||
|
.fetch();
|
||||||
|
this.dbi.setMapSize(GROW_SIZE);
|
||||||
|
this.sectionDatabase = this.dbi.createDb("world_sections");
|
||||||
|
this.idMappingDatabase = this.dbi.createDb("id_mapping");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void growEnv() {
|
||||||
|
long size = this.dbi.getMapSize() + GROW_SIZE;
|
||||||
|
System.out.println("Growing DBI env size to: " + size + " bytes");
|
||||||
|
this.dbi.setMapSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: try optimize this hellscape of spagetti locking
|
||||||
|
private <T> T resizingTransaction(Supplier<T> transaction) {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return this.synchronizedTransaction(transaction);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (e.getMessage().startsWith("Code: -30792")) {
|
||||||
|
if (this.resizeLock.tryLock()) {
|
||||||
|
//We must wait until all the other transactions have finished before we can resize
|
||||||
|
while (this.accessingCounts.get() != 0) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.growEnv();
|
||||||
|
|
||||||
|
this.resizeLock.unlock();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T synchronizedTransaction(Supplier<T> transaction) {
|
||||||
|
try {
|
||||||
|
this.accessingCounts.getAndAdd(1);
|
||||||
|
//Check if its locked, if it is locked then need to release the access, wait till resize is finished then
|
||||||
|
while (this.resizeLock.isLocked()) {
|
||||||
|
this.accessingCounts.getAndAdd(-1);
|
||||||
|
while (this.resizeLock.isLocked()) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
this.accessingCounts.getAndAdd(1);
|
||||||
|
}
|
||||||
|
return transaction.get();
|
||||||
|
} finally {
|
||||||
|
this.accessingCounts.getAndAdd(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: make batch get and updates
|
||||||
|
public byte[] getSectionData(long key) {
|
||||||
|
return this.synchronizedTransaction(() -> this.sectionDatabase.transaction(MDB_RDONLY, transaction->{
|
||||||
|
var buff = transaction.stack.malloc(8);
|
||||||
|
buff.putLong(0, key);
|
||||||
|
var bb = transaction.get(buff);
|
||||||
|
if (bb == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var res = new byte[bb.remaining()];
|
||||||
|
bb.get(res);
|
||||||
|
return res;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: pad data to like some alignemnt so that when the section gets saved or updated
|
||||||
|
// it can use the same allocation
|
||||||
|
public void setSectionData(long key, ByteBuffer data) {
|
||||||
|
this.resizingTransaction(() -> this.sectionDatabase.transaction(transaction->{
|
||||||
|
var keyBuff = transaction.stack.malloc(8);
|
||||||
|
keyBuff.putLong(0, key);
|
||||||
|
transaction.put(keyBuff, data, 0);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteSectionData(long key) {
|
||||||
|
this.synchronizedTransaction(() -> this.sectionDatabase.transaction(transaction->{
|
||||||
|
var keyBuff = transaction.stack.malloc(8);
|
||||||
|
keyBuff.putLong(0, key);
|
||||||
|
transaction.del(keyBuff);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putIdMapping(int id, ByteBuffer data) {
|
||||||
|
this.resizingTransaction(()->this.idMappingDatabase.transaction(transaction->{
|
||||||
|
var keyBuff = transaction.stack.malloc(4);
|
||||||
|
keyBuff.putInt(0, id);
|
||||||
|
transaction.put(keyBuff, data, 0);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int2ObjectOpenHashMap<byte[]> getIdMappings() {
|
||||||
|
return this.synchronizedTransaction(() -> {
|
||||||
|
Int2ObjectOpenHashMap<byte[]> mapping = new Int2ObjectOpenHashMap<>();
|
||||||
|
this.idMappingDatabase.transaction(MDB_RDONLY, transaction -> {
|
||||||
|
try (var cursor = transaction.createCursor()) {
|
||||||
|
var keyPtr = MDBVal.malloc(transaction.stack);
|
||||||
|
var valPtr = MDBVal.malloc(transaction.stack);
|
||||||
|
while (cursor.get(MDB_NEXT, keyPtr, valPtr) != MDB_NOTFOUND) {
|
||||||
|
int keyVal = keyPtr.mv_data().getInt(0);
|
||||||
|
byte[] data = new byte[(int) valPtr.mv_size()];
|
||||||
|
Objects.requireNonNull(valPtr.mv_data()).get(data);
|
||||||
|
mapping.put(keyVal, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return mapping;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flush() {
|
||||||
|
this.dbi.flush(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
this.sectionDatabase.close();
|
||||||
|
this.idMappingDatabase.close();
|
||||||
|
this.dbi.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
import org.lwjgl.system.MemoryStack;
|
||||||
|
|
||||||
|
public interface TransactionCallback<T> {
|
||||||
|
T exec(MemoryStack stack, long transaction);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
public interface TransactionWrappedCallback<T> {
|
||||||
|
T exec(TransactionWrapper wrapper);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package me.cortex.voxelmon.core.world.storage;
|
||||||
|
|
||||||
|
import org.lwjgl.PointerBuffer;
|
||||||
|
import org.lwjgl.system.MemoryStack;
|
||||||
|
import org.lwjgl.util.lmdb.MDBVal;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import static me.cortex.voxelmon.core.world.storage.LMDBInterface.E;
|
||||||
|
import static org.lwjgl.system.MemoryStack.stackPush;
|
||||||
|
import static org.lwjgl.util.lmdb.LMDB.*;
|
||||||
|
|
||||||
|
public class TransactionWrapper {
|
||||||
|
public final MemoryStack stack;
|
||||||
|
private final long transaction;
|
||||||
|
private int dbi;
|
||||||
|
|
||||||
|
public TransactionWrapper(long transaction, MemoryStack stack) {
|
||||||
|
this.transaction = transaction;
|
||||||
|
this.stack = stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionWrapper set(LMDBInterface.Database db) {
|
||||||
|
this.dbi = db.getDBI();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionWrapper put(ByteBuffer key, ByteBuffer val, int flags) {
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
E(mdb_put(this.transaction, this.dbi, MDBVal.malloc(stack).mv_data(key), MDBVal.malloc(stack).mv_data(val), flags));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionWrapper del(ByteBuffer key) {
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
E(mdb_del(this.transaction, this.dbi, MDBVal.malloc(stack).mv_data(key), null));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public TransactionWrapper put(long keyPtr, long keyLen, long valPtr, int valLen, int flags) {
|
||||||
|
//TODO: instead give TransactionWrapper its own scratch buffer that it can use
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
long ptr = stack.nmalloc(4*8);
|
||||||
|
MemoryUtil.memPutLong(ptr, keyPtr);
|
||||||
|
MemoryUtil.memPutLong(ptr+8, keyLen);
|
||||||
|
MemoryUtil.memPutLong(ptr+16, valPtr);
|
||||||
|
MemoryUtil.memPutLong(ptr+24, valLen);
|
||||||
|
E(nmdb_put(this.transaction, this.dbi, ptr, ptr + 16, flags));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public ByteBuffer get(ByteBuffer key) {
|
||||||
|
//TODO: instead give TransactionWrapper its own scratch buffer that it can use
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
var ret = MDBVal.malloc(stack);
|
||||||
|
int retVal = mdb_get(this.transaction, this.dbi, MDBVal.calloc(stack).mv_data(key), ret);
|
||||||
|
if (retVal == MDB_NOTFOUND) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
E(retVal);
|
||||||
|
}
|
||||||
|
return ret.mv_data();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor createCursor() {
|
||||||
|
try (var stack = stackPush()) {
|
||||||
|
PointerBuffer pb = stack.mallocPointer(1);
|
||||||
|
E(mdb_cursor_open(transaction, dbi, pb));
|
||||||
|
return new Cursor(pb.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package me.cortex.voxelmon.importers;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.util.math.ChunkPos;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
|
||||||
|
public class BobbyImporter {
|
||||||
|
private final WorldEngine world;
|
||||||
|
private final World mcWorld;
|
||||||
|
public BobbyImporter(WorldEngine worldEngine, World mcWorld) {
|
||||||
|
this.world = worldEngine;
|
||||||
|
this.mcWorld = mcWorld;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: make the importer run in another thread with a progress callback
|
||||||
|
public void importBobby(Path directory) {
|
||||||
|
directory.forEach(child->{
|
||||||
|
var file = child.toFile();
|
||||||
|
if (!file.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var name = file.getName();
|
||||||
|
var sections = name.split("\\.");
|
||||||
|
if (sections.length != 4 || (!sections[0].equals("r")) || (!sections[3].equals("mca"))) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
int rx = Integer.parseInt(sections[1]);
|
||||||
|
int rz = Integer.parseInt(sections[2]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importRegionFile(Path file, int x, int z) throws IOException {
|
||||||
|
try (var fileStream = FileChannel.open(file, StandardOpenOption.READ)) {
|
||||||
|
var sectorsSavesBB = MemoryUtil.memAlloc(8192);
|
||||||
|
if (fileStream.read(sectorsSavesBB) != 8192) {
|
||||||
|
throw new IllegalStateException("Header of region file invalid");
|
||||||
|
}
|
||||||
|
var sectorsSaves = sectorsSavesBB.asIntBuffer();
|
||||||
|
|
||||||
|
//Find and load all saved chunks
|
||||||
|
for (int idx = 0; idx < 1024; idx++) {
|
||||||
|
int sectorMeta = sectorsSaves.get(idx);
|
||||||
|
if (sectorMeta == 0) {
|
||||||
|
//Empty chunk
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int sectorStart = sectorMeta>>>8;
|
||||||
|
int sectorCount = sectorMeta&((1<<8)-1);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importChunkNBT(ChunkPos pos, NbtCompound chunk) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/main/java/me/cortex/voxelmon/importers/WorldImporter.java
Normal file
180
src/main/java/me/cortex/voxelmon/importers/WorldImporter.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package me.cortex.voxelmon.importers;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import me.cortex.voxelmon.core.util.ByteBufferBackedInputStream;
|
||||||
|
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
|
||||||
|
import me.cortex.voxelmon.core.voxelization.WorldConversionFactory;
|
||||||
|
import me.cortex.voxelmon.core.world.WorldEngine;
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.block.BlockState;
|
||||||
|
import net.minecraft.block.Blocks;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.nbt.NbtElement;
|
||||||
|
import net.minecraft.nbt.NbtIo;
|
||||||
|
import net.minecraft.nbt.NbtOps;
|
||||||
|
import net.minecraft.registry.RegistryKeys;
|
||||||
|
import net.minecraft.registry.entry.RegistryEntry;
|
||||||
|
import net.minecraft.util.math.ChunkPos;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import net.minecraft.world.biome.Biome;
|
||||||
|
import net.minecraft.world.biome.BiomeKeys;
|
||||||
|
import net.minecraft.world.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.chunk.ReadableContainer;
|
||||||
|
import net.minecraft.world.storage.ChunkStreamVersion;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class WorldImporter {
|
||||||
|
private final WorldEngine world;
|
||||||
|
private final World mcWorld;
|
||||||
|
private final Codec<ReadableContainer<RegistryEntry<Biome>>> biomeCodec;
|
||||||
|
|
||||||
|
public WorldImporter(WorldEngine worldEngine, World mcWorld) {
|
||||||
|
this.world = worldEngine;
|
||||||
|
this.mcWorld = mcWorld;
|
||||||
|
|
||||||
|
var biomeRegistry = mcWorld.getRegistryManager().get(RegistryKeys.BIOME);
|
||||||
|
this.biomeCodec = PalettedContainer.createReadableContainerCodec(biomeRegistry.getIndexedEntries(), biomeRegistry.createEntryCodec(), PalettedContainer.PaletteProvider.BIOME, biomeRegistry.entryOf(BiomeKeys.PLAINS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread worker;
|
||||||
|
public void importWorldAsyncStart(File directory) {
|
||||||
|
this.worker = new Thread(() -> {
|
||||||
|
Arrays.stream(directory.listFiles()).parallel().forEach(file -> {
|
||||||
|
if (!file.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var name = file.getName();
|
||||||
|
var sections = name.split("\\.");
|
||||||
|
if (sections.length != 4 || (!sections[0].equals("r")) || (!sections[3].equals("mca"))) {
|
||||||
|
System.err.println("Unknown file: " + name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int rx = Integer.parseInt(sections[1]);
|
||||||
|
int rz = Integer.parseInt(sections[2]);
|
||||||
|
try {
|
||||||
|
this.importRegionFile(file.toPath(), rx, rz);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
System.err.println("Done");
|
||||||
|
});
|
||||||
|
this.worker.setName("World importer");
|
||||||
|
this.worker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importRegionFile(Path file, int x, int z) throws IOException {
|
||||||
|
try (var fileStream = FileChannel.open(file, StandardOpenOption.READ)) {
|
||||||
|
var sectorsSavesBB = MemoryUtil.memAlloc(8192);
|
||||||
|
if (fileStream.read(sectorsSavesBB, 0) != 8192) {
|
||||||
|
System.err.println("Header of region file invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sectorsSavesBB.rewind();
|
||||||
|
var sectorsSaves = sectorsSavesBB.order(ByteOrder.BIG_ENDIAN).asIntBuffer();
|
||||||
|
|
||||||
|
//Find and load all saved chunks
|
||||||
|
for (int idx = 0; idx < 1024; idx++) {
|
||||||
|
int sectorMeta = sectorsSaves.get(idx);
|
||||||
|
if (sectorMeta == 0) {
|
||||||
|
//Empty chunk
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int sectorStart = sectorMeta>>>8;
|
||||||
|
int sectorCount = sectorMeta&((1<<8)-1);
|
||||||
|
var data = MemoryUtil.memAlloc(sectorCount*4096).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
fileStream.read(data, sectorStart*4096L);
|
||||||
|
data.flip();
|
||||||
|
{
|
||||||
|
int m = data.getInt();
|
||||||
|
byte b = data.get();
|
||||||
|
if (m == 0) {
|
||||||
|
System.err.println("Chunk is allocated, but stream is missing");
|
||||||
|
} else {
|
||||||
|
int n = m - 1;
|
||||||
|
if ((b & 128) != 0) {
|
||||||
|
if (n != 0) {
|
||||||
|
System.err.println("Chunk has both internal and external streams");
|
||||||
|
}
|
||||||
|
System.err.println("Chunk has external stream which is not supported");
|
||||||
|
} else if (n > data.remaining()) {
|
||||||
|
System.err.println("Chunk stream is truncated: expected "+n+" but read " + data.remaining());
|
||||||
|
} else if (n < 0) {
|
||||||
|
System.err.println("Declared size of chunk is negative");
|
||||||
|
} else {
|
||||||
|
try (var decompressedData = this.decompress(b, new ByteBufferBackedInputStream(data))) {
|
||||||
|
if (decompressedData == null) {
|
||||||
|
System.err.println("Error decompressing chunk data");
|
||||||
|
} else {
|
||||||
|
var nbt = NbtIo.readCompound(decompressedData);
|
||||||
|
this.importChunkNBT(nbt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryUtil.memFree(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryUtil.memFree(sectorsSavesBB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataInputStream decompress(byte flags, InputStream stream) throws IOException {
|
||||||
|
ChunkStreamVersion chunkStreamVersion = ChunkStreamVersion.get(flags);
|
||||||
|
if (chunkStreamVersion == null) {
|
||||||
|
System.err.println("Chunk has invalid chunk stream version");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return new DataInputStream(chunkStreamVersion.wrap(stream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importChunkNBT(NbtCompound chunk) {
|
||||||
|
try {
|
||||||
|
int x = chunk.getInt("xPos");
|
||||||
|
int z = chunk.getInt("zPos");
|
||||||
|
for (var sectionE : chunk.getList("sections", NbtElement.COMPOUND_TYPE)) {
|
||||||
|
var section = (NbtCompound) sectionE;
|
||||||
|
int y = section.getInt("Y");
|
||||||
|
this.importSectionNBT(x, y, z, section);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Exception importing world chunk:");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.createPalettedContainerCodec(Block.STATE_IDS, BlockState.CODEC, PalettedContainer.PaletteProvider.BLOCK_STATE, Blocks.AIR.getDefaultState());
|
||||||
|
private void importSectionNBT(int x, int y, int z, NbtCompound section) {
|
||||||
|
if (section.getCompound("block_states").isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockStates = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, section.getCompound("block_states")).result().get();
|
||||||
|
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes")).result().get();
|
||||||
|
|
||||||
|
VoxelizedSection csec = WorldConversionFactory.convert(
|
||||||
|
this.world.getMapper(),
|
||||||
|
blockStates,
|
||||||
|
biomes,
|
||||||
|
(bx, by, bz) -> (byte) 0,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z
|
||||||
|
);
|
||||||
|
|
||||||
|
this.world.insertUpdate(csec);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.joml;
|
||||||
|
|
||||||
|
import org.joml.FrustumIntersection;
|
||||||
|
import org.joml.Vector4f;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(value = FrustumIntersection.class, remap = false)
|
||||||
|
public interface AccessFrustumIntersection {
|
||||||
|
@Accessor Vector4f[] getPlanes();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import net.minecraft.client.render.BackgroundRenderer;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Constant;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyConstant;
|
||||||
|
|
||||||
|
@Mixin(BackgroundRenderer.class)
|
||||||
|
|
||||||
|
public class MixinBackgroundRenderer {
|
||||||
|
@ModifyConstant(method = "applyFog", constant = @Constant(floatValue = 192.0F))
|
||||||
|
private static float changeFog(float fog) {
|
||||||
|
return 9999999f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.VoxelCore;
|
||||||
|
import net.minecraft.client.world.ClientChunkManager;
|
||||||
|
import net.minecraft.util.math.ChunkPos;
|
||||||
|
import net.minecraft.world.chunk.WorldChunk;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||||
|
|
||||||
|
@Mixin(ClientChunkManager.class)
|
||||||
|
public class MixinClientChunkManager {
|
||||||
|
@Inject(method = "unload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;compareAndSet(ILnet/minecraft/world/chunk/WorldChunk;Lnet/minecraft/world/chunk/WorldChunk;)Lnet/minecraft/world/chunk/WorldChunk;", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
|
||||||
|
private void injectUnload(ChunkPos pos, CallbackInfo ci, int index, WorldChunk worldChunk) {
|
||||||
|
//VoxelCore.INSTANCE.enqueueIngest(worldChunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.core.VoxelCore;
|
||||||
|
import net.minecraft.client.gui.hud.DebugHud;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mixin(DebugHud.class)
|
||||||
|
public class MixinDebugHud {
|
||||||
|
@Inject(method = "getRightText", at = @At("TAIL"))
|
||||||
|
private void injectDebug(CallbackInfoReturnable<List<String>> cir) {
|
||||||
|
var ret = cir.getReturnValue();
|
||||||
|
VoxelCore.INSTANCE.addDebugInfo(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import net.minecraft.client.render.GameRenderer;
|
||||||
|
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(GameRenderer.class)
|
||||||
|
public class MixinGameRenderer {
|
||||||
|
@Inject(method = "getFarPlaneDistance", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void method_32796(CallbackInfoReturnable<Float> cir) {
|
||||||
|
cir.setReturnValue(16 * 3000f);
|
||||||
|
cir.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.RunArgs;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(MinecraftClient.class)
|
||||||
|
public class MixinMinecraftClient {
|
||||||
|
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resource/PeriodicNotificationManager;<init>(Lnet/minecraft/util/Identifier;Lit/unimi/dsi/fastutil/objects/Object2BooleanFunction;)V", shift = At.Shift.AFTER))
|
||||||
|
private void injectRenderDoc(RunArgs args, CallbackInfo ci) {
|
||||||
|
//System.load("C:\\Program Files\\RenderDoc\\renderdoc.dll");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package me.cortex.voxelmon.mixin.minecraft;
|
||||||
|
|
||||||
|
import me.cortex.voxelmon.Voxelmon;
|
||||||
|
import me.cortex.voxelmon.core.VoxelCore;
|
||||||
|
import net.minecraft.client.render.*;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
@Mixin(WorldRenderer.class)
|
||||||
|
public abstract class MixinWorldRenderer {
|
||||||
|
@Shadow protected abstract void renderLayer(RenderLayer renderLayer, MatrixStack matrices, double cameraX, double cameraY, double cameraZ, Matrix4f positionMatrix);
|
||||||
|
|
||||||
|
@Shadow protected abstract void setupTerrain(Camera camera, Frustum frustum, boolean hasForcedFrustum, boolean spectator);
|
||||||
|
|
||||||
|
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V"))
|
||||||
|
private void injectSetup(WorldRenderer instance, Camera camera, Frustum frustum, boolean hasForcedFrustum, boolean spectator) {
|
||||||
|
//Call the actual terrain setup method
|
||||||
|
this.setupTerrain(camera, frustum, hasForcedFrustum, spectator);
|
||||||
|
//Call our setup method
|
||||||
|
VoxelCore.INSTANCE.renderSetup(frustum, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;renderLayer(Lnet/minecraft/client/render/RenderLayer;Lnet/minecraft/client/util/math/MatrixStack;DDDLorg/joml/Matrix4f;)V", ordinal = 2))
|
||||||
|
private void injectOpaqueRender(WorldRenderer instance, RenderLayer renderLayer, MatrixStack matrices, double cameraX, double cameraY, double cameraZ, Matrix4f positionMatrix) {
|
||||||
|
//Call the actual render method
|
||||||
|
this.renderLayer(renderLayer, matrices, cameraX, cameraY, cameraZ, positionMatrix);
|
||||||
|
VoxelCore.INSTANCE.renderOpaque(matrices, cameraX, cameraY, cameraZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F"))
|
||||||
|
private float redirectMax(float a, float b) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getViewDistance()F"))
|
||||||
|
private float changeRD(GameRenderer instance) {
|
||||||
|
float viewDistance = instance.getViewDistance();
|
||||||
|
return 16*1512;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
struct Frustum {
|
||||||
|
vec4 planes[6];
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 0, std140) uniform SceneUniform {
|
||||||
|
mat4 MVP;
|
||||||
|
ivec3 baseSectionPos;
|
||||||
|
int sectionCount;
|
||||||
|
Frustum frustum;
|
||||||
|
vec3 cameraSubPos;
|
||||||
|
int _padA;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
uint biomeTintMsk;
|
||||||
|
uint faceColours[6];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Biome {
|
||||||
|
uint foliage;
|
||||||
|
uint water;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SectionMeta {
|
||||||
|
uvec4 header;
|
||||||
|
uvec4 drawdata;
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO: see if making the stride 2*4*4 bytes or something cause you get that 16 byte write
|
||||||
|
struct DrawCommand {
|
||||||
|
uint count;
|
||||||
|
uint instanceCount;
|
||||||
|
uint firstIndex;
|
||||||
|
int baseVertex;
|
||||||
|
uint baseInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 1, std430) readonly restrict buffer QuadBuffer {
|
||||||
|
Quad quadData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 2, std430) writeonly restrict buffer DrawBuffer {
|
||||||
|
DrawCommand cmdBuffer[];
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 3, std430) readonly restrict buffer SectionBuffer {
|
||||||
|
SectionMeta sectionData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 4, std430) readonly restrict buffer StateBuffer {
|
||||||
|
State stateData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(binding = 5, std430) readonly restrict buffer BiomeBuffer {
|
||||||
|
Biome biomeData[];
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#version 460
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : enable
|
||||||
|
#import <voxelmon:lod/gl46/quad_format.glsl>
|
||||||
|
#import <voxelmon:lod/gl46/bindings.glsl>
|
||||||
|
|
||||||
|
//https://github.com/KhronosGroup/GLSL/blob/master/extensions/ext/GL_EXT_shader_16bit_storage.txt
|
||||||
|
// adds support for uint8_t which can use for compact visibility buffer
|
||||||
|
|
||||||
|
layout(local_size_x = 128, local_size_y = 1, local_size_x = 1) in;
|
||||||
|
|
||||||
|
|
||||||
|
bool testFrustumPoint(vec4 plane, vec3 min, vec3 max) {
|
||||||
|
vec3 point = mix(max, min, lessThan(plane.xyz, vec3(0))) * plane.xyz;
|
||||||
|
return (point.x + point.y + point.z) >= -plane.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool testFrustum(Frustum frust, vec3 min, vec3 max) {
|
||||||
|
return testFrustumPoint(frust.planes[0], min, max) &&
|
||||||
|
testFrustumPoint(frust.planes[1], min, max) &&
|
||||||
|
testFrustumPoint(frust.planes[2], min, max) &&
|
||||||
|
testFrustumPoint(frust.planes[3], min, max) &&
|
||||||
|
testFrustumPoint(frust.planes[4], min, max) &&
|
||||||
|
testFrustumPoint(frust.planes[5], min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
uint extractDetail(SectionMeta section) {
|
||||||
|
return section.header.x>>28;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
uint count;
|
||||||
|
uint instanceCount;
|
||||||
|
uint firstIndex;
|
||||||
|
int baseVertex;
|
||||||
|
uint baseInstance;
|
||||||
|
*/
|
||||||
|
ivec3 extractPosition(SectionMeta section) {
|
||||||
|
int y = ((int(section.header.x)<<4)>>24);
|
||||||
|
int x = (int(section.header.y)<<4)>>8;
|
||||||
|
int z = int((section.header.x&((1<<20)-1))<<4);
|
||||||
|
z |= int(section.header.y>>28);
|
||||||
|
z <<= 8;
|
||||||
|
z >>= 8;
|
||||||
|
return ivec3(x,y,z);
|
||||||
|
}
|
||||||
|
uint extractQuadStart(SectionMeta meta) {
|
||||||
|
return meta.header.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractQuadCount(SectionMeta meta) {
|
||||||
|
return meta.header.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint encodeLocalLodPos(uint detail, ivec3 pos) {
|
||||||
|
uvec3 detla = (pos - (baseSectionPos >> detail))&((1<<9)-1);
|
||||||
|
return (detail<<29)|(detla.x<<20)|(detla.y<<11)|(detla.z<<2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (gl_GlobalInvocationID.x >= sectionCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SectionMeta meta = sectionData[gl_GlobalInvocationID.x];
|
||||||
|
uint detail = extractDetail(meta);
|
||||||
|
ivec3 ipos = extractPosition(meta);
|
||||||
|
|
||||||
|
//TODO: fixme; i dont think this is correct
|
||||||
|
vec3 cornerPos = vec3(((ipos<<detail)-baseSectionPos)<<5)-cameraSubPos;
|
||||||
|
|
||||||
|
|
||||||
|
bool shouldRender = testFrustum(frustum, cornerPos, cornerPos+vec3(1<<(detail+5)));
|
||||||
|
|
||||||
|
//This prevents overflow of the relative position encoder
|
||||||
|
if (shouldRender) {
|
||||||
|
shouldRender = !any(lessThan(ivec3(254), abs(ipos-(baseSectionPos>>detail))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRender) {
|
||||||
|
DrawCommand cmd;
|
||||||
|
cmd.count = extractQuadCount(meta) * 6;
|
||||||
|
cmd.instanceCount = 1;
|
||||||
|
cmd.firstIndex = 0;
|
||||||
|
cmd.baseVertex = int(extractQuadStart(meta))<<2;
|
||||||
|
cmd.baseInstance = encodeLocalLodPos(detail, ipos);
|
||||||
|
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
|
||||||
|
} else {
|
||||||
|
DrawCommand cmd;
|
||||||
|
cmd.count = 0;
|
||||||
|
cmd.instanceCount = 0;
|
||||||
|
cmd.firstIndex = 0;
|
||||||
|
cmd.baseVertex = 0;
|
||||||
|
cmd.baseInstance = 0;
|
||||||
|
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : enable
|
||||||
|
|
||||||
|
#import <voxelmon:lod/gl46/bindings.glsl>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : enable
|
||||||
|
|
||||||
|
#import <voxelmon:lod/gl46/bindings.glsl>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifdef GL_ARB_gpu_shader_int64
|
||||||
|
#define Quad uint64_t
|
||||||
|
|
||||||
|
#define Eu32(data, amountBits, shift) (uint((data)>>(shift))&((1u<<(amountBits))-1))
|
||||||
|
|
||||||
|
vec3 extractPos(uint64_t quad) {
|
||||||
|
//TODO: pull out the majic constants into #defines (specifically the shift amount)
|
||||||
|
return vec3(Eu32(quad, 5, 21), Eu32(quad, 5, 16), Eu32(quad, 5, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
ivec2 extractSize(uint64_t quad) {
|
||||||
|
return ivec2(Eu32(quad, 4, 3), Eu32(quad, 4, 7)) + ivec2(1);//the + 1 is cause you cant actually have a 0 size quad
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractFace(uint64_t quad) {
|
||||||
|
return Eu32(quad, 3, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractStateId(uint64_t quad) {
|
||||||
|
return Eu32(quad, 20, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractBiomeId(uint64_t quad) {
|
||||||
|
return Eu32(quad, 9, 46);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractLightId(uint64_t quad) {
|
||||||
|
return Eu32(quad, 8, 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
//TODO: FIXME, ivec2 swaps around the data of the x and y cause its written in little endian
|
||||||
|
|
||||||
|
#define Quad ivec2
|
||||||
|
|
||||||
|
#define Eu32(data, amountBits, shift) (uint((data)>>(shift))&((1u<<(amountBits))-1))
|
||||||
|
|
||||||
|
vec3 extractPos(ivec2 quad) {
|
||||||
|
return vec3(Eu32(quad.y, 5, 21), Eu32(quad.y, 5, 16), Eu32(quad.y, 5, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
ivec2 extractSize(ivec2 quad) {
|
||||||
|
return ivec2(Eu32(quad.y, 4, 3), Eu32(quad.y, 4, 7)) + ivec2(1);//the + 1 is cause you cant actually have a 0 size quad
|
||||||
|
}
|
||||||
|
|
||||||
|
uint extractFace(ivec2 quad) {
|
||||||
|
return quad.y&7;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#version 460 core
|
||||||
|
layout(location = 0) in flat vec4 colour;
|
||||||
|
layout(location = 0) out vec4 outColour;
|
||||||
|
void main() {
|
||||||
|
//TODO: randomly discard the fragment with respect to the alpha value
|
||||||
|
|
||||||
|
outColour = colour;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#version 460 core
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : enable
|
||||||
|
|
||||||
|
#import <voxelmon:lod/gl46/quad_format.glsl>
|
||||||
|
#import <voxelmon:lod/gl46/bindings.glsl>
|
||||||
|
|
||||||
|
layout(location = 0) out flat vec4 colour;
|
||||||
|
|
||||||
|
uint extractLodLevel() {
|
||||||
|
return uint(gl_BaseInstance)>>29;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Note the last 2 bits of gl_BaseInstance are unused
|
||||||
|
//Gives a relative position of +-255 relative to the player center in its respective lod
|
||||||
|
ivec3 extractRelativeLodPos() {
|
||||||
|
return (ivec3(gl_BaseInstance)<<ivec3(3,12,21))>>ivec3(23);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 uint2vec4RGBA(uint colour) {
|
||||||
|
return vec4((uvec4(colour)>>uvec4(24,16,8,0))&uvec4(0xFF))/255;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int cornerIdx = gl_VertexID&3;
|
||||||
|
|
||||||
|
Quad quad = quadData[uint(gl_VertexID)>>2];
|
||||||
|
vec3 innerPos = extractPos(quad);
|
||||||
|
|
||||||
|
uint face = extractFace(quad);
|
||||||
|
|
||||||
|
uint lodLevel = extractLodLevel();
|
||||||
|
ivec3 lodCorner = ((extractRelativeLodPos()<<lodLevel) - (baseSectionPos&(ivec3((1<<lodLevel)-1))))<<5;
|
||||||
|
vec3 corner = innerPos * (1<<lodLevel) + lodCorner;
|
||||||
|
|
||||||
|
//TODO: see if backface culling is even needed, since everything (should) be back culled already
|
||||||
|
//Flip the quad rotation by its face (backface culling)
|
||||||
|
if ((face&1) != 0) {
|
||||||
|
cornerIdx ^= 1;
|
||||||
|
}
|
||||||
|
if ((face>>1) == 0) {
|
||||||
|
cornerIdx ^= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ivec2 size = extractSize(quad) * ivec2((cornerIdx>>1)&1, cornerIdx&1) * (1<<lodLevel);
|
||||||
|
|
||||||
|
vec3 pos = corner;
|
||||||
|
|
||||||
|
//NOTE: can just make instead of face, make it axis (can also make it 2 bit instead of 3 bit then)
|
||||||
|
// since the only reason face is needed is to ensure backface culling orientation thing
|
||||||
|
uint axis = face>>1;
|
||||||
|
if (axis == 0) {
|
||||||
|
pos.xz += size;
|
||||||
|
pos.y += (face&1)<<lodLevel;
|
||||||
|
} else if (axis == 1) {
|
||||||
|
pos.xy += size;
|
||||||
|
pos.z += (face&1)<<lodLevel;
|
||||||
|
} else {
|
||||||
|
pos.yz += size;
|
||||||
|
pos.x += (face&1)<<lodLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = MVP * vec4(pos,1);
|
||||||
|
|
||||||
|
uint stateId = extractStateId(quad);
|
||||||
|
uint biomeId = extractBiomeId(quad);
|
||||||
|
State stateInfo = stateData[stateId];
|
||||||
|
colour = uint2vec4RGBA(stateInfo.faceColours[face]);
|
||||||
|
if (((stateInfo.biomeTintMsk>>face)&1) == 1) {
|
||||||
|
vec4 biomeColour = uint2vec4RGBA(biomeData[biomeId].foliage);
|
||||||
|
colour *= biomeColour;
|
||||||
|
}
|
||||||
|
//Apply water tint
|
||||||
|
if (((stateInfo.biomeTintMsk>>6)&1) == 1) {
|
||||||
|
colour *= vec4(0.247, 0.463, 0.894, 1);
|
||||||
|
}
|
||||||
|
//gl_Position = MVP * vec4(vec3(((cornerIdx)&1)+10,10,((cornerIdx>>1)&1)+10),1);
|
||||||
|
//uint i = uint(quad>>32);
|
||||||
|
uint i = uint(gl_BaseInstance);
|
||||||
|
i ^= i>>16;
|
||||||
|
i *= 124128573;
|
||||||
|
i ^= i>>16;
|
||||||
|
i *= 4211346123;
|
||||||
|
i ^= i>>16;
|
||||||
|
i *= 462312435;
|
||||||
|
i ^= i>>16;
|
||||||
|
i *= 542354341;
|
||||||
|
i ^= i>>16;
|
||||||
|
|
||||||
|
//uint i = uint(lodLevel+12)*215387625;
|
||||||
|
//colour *= vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7)*0.7+0.3,1);
|
||||||
|
//colour = vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7),1);
|
||||||
|
}
|
||||||
8
src/main/resources/assets/voxelmon/shaders/quads.frag
Normal file
8
src/main/resources/assets/voxelmon/shaders/quads.frag
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#version 460 core
|
||||||
|
layout(location = 0) in flat vec4 colour;
|
||||||
|
layout(location = 0) out vec4 outColour;
|
||||||
|
void main() {
|
||||||
|
//TODO: randomly discard the fragment with respect to the alpha value
|
||||||
|
|
||||||
|
outColour = colour;
|
||||||
|
}
|
||||||
18
src/main/resources/assets/voxelmon/shaders/quads.vert
Normal file
18
src/main/resources/assets/voxelmon/shaders/quads.vert
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#version 460 core
|
||||||
|
|
||||||
|
layout(location = 0) out flat vec4 colour;
|
||||||
|
layout(binding = 0, std140) uniform SceneUniform {
|
||||||
|
mat4 MVP;
|
||||||
|
ivec3 baseSectionPos;
|
||||||
|
int _pad1;
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int cornerIdx = gl_VertexID&3;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
gl_Position = MVP * vec4(vec3(cornerIdx&1,((cornerIdx>>1)&1),0),1);
|
||||||
|
|
||||||
|
colour = vec4(float((uint(gl_VertexID)>>2)&7)/7,float((uint(gl_VertexID)>>5)&7)/7,float((uint(gl_VertexID)>>8)&7)/7,1);
|
||||||
|
}
|
||||||
28
src/main/resources/assets/voxelmon/shaders/ray/ray.glsl
Normal file
28
src/main/resources/assets/voxelmon/shaders/ray/ray.glsl
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//Contains the definision of a ray and step functions
|
||||||
|
struct Ray {
|
||||||
|
ivec3 pos;
|
||||||
|
vec3 innerPos;
|
||||||
|
|
||||||
|
vec3 dir;
|
||||||
|
vec3 invDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
Ray ray;
|
||||||
|
void setup(vec3 origin, vec3 direction) {
|
||||||
|
ray.pos = ivec3(origin);
|
||||||
|
ray.innerPos = origin - ray.pos;
|
||||||
|
direction *= inversesqrt(direction);
|
||||||
|
ray.dir = direction;
|
||||||
|
ray.invDir = 1/direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
void step(ivec3 aabb) {
|
||||||
|
//TODO:check for innerPos>=1 and step into that voxel
|
||||||
|
vec3 t2f = (aabb - ray.innerPos) * ray.invDir;
|
||||||
|
float mint2f = min(t2f.x, min(t2f.y, t2f.z));
|
||||||
|
bvec3 msk = lessThanEqual(t2f.xyz, vec3(mint2f));
|
||||||
|
vec3 newIP = mint2f * ray.dir + ray.innerPos;
|
||||||
|
ivec3 offset = min(aabb-1, ivec3(newIP));
|
||||||
|
ray.pos += offset + ivec3(msk);
|
||||||
|
ray.innerPos = mix(vec3(0), newIP - offset, not(msk));
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
struct Voxel {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO: add tlas and blas voxel fetching (rings and all)
|
||||||
|
void getVoxel() {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
//Glue code between ray stepper and voxel storage
|
||||||
|
// its the primary ray tracer
|
||||||
21
src/main/resources/fabric.mod.json
Normal file
21
src/main/resources/fabric.mod.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "voxelmon",
|
||||||
|
"version": "${version}",
|
||||||
|
"name": "voxelmon",
|
||||||
|
"description": "",
|
||||||
|
"authors": [],
|
||||||
|
"contact": {
|
||||||
|
},
|
||||||
|
"license": "All-Rights-Reserved",
|
||||||
|
"icon": "assets/voxelmon/icon.png",
|
||||||
|
"environment": "client",
|
||||||
|
"entrypoints": {
|
||||||
|
},
|
||||||
|
"mixins": [
|
||||||
|
"voxelmon.mixins.json"
|
||||||
|
],
|
||||||
|
"depends": {
|
||||||
|
"fabricloader": ">=0.14.22"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/main/resources/voxelmon.accesswidener
Normal file
4
src/main/resources/voxelmon.accesswidener
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
accessWidener v1 named
|
||||||
|
|
||||||
|
accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage;
|
||||||
|
accessible field net/minecraft/client/render/Frustum frustumIntersection Lorg/joml/FrustumIntersection;
|
||||||
19
src/main/resources/voxelmon.mixins.json
Normal file
19
src/main/resources/voxelmon.mixins.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"package": "me.cortex.voxelmon.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_17",
|
||||||
|
"client": [
|
||||||
|
"minecraft.MixinBackgroundRenderer",
|
||||||
|
"minecraft.MixinClientChunkManager",
|
||||||
|
"minecraft.MixinDebugHud",
|
||||||
|
"minecraft.MixinGameRenderer",
|
||||||
|
"minecraft.MixinMinecraftClient",
|
||||||
|
"minecraft.MixinWorldRenderer"
|
||||||
|
],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
},
|
||||||
|
"mixins": [
|
||||||
|
"joml.AccessFrustumIntersection"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user