diff --git a/.editorconfig b/.editorconfig index 038bc765e..294e122e9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,9 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.yml] +indent_size = 2 + [*.json] indent_size = 2 max_line_length = 500 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a538b35b..b83ac8402 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,47 +1,85 @@ -name: build +name: Build -on: [ pull_request, push ] +on: [ workflow_dispatch, pull_request, push ] + +env: + JAVA_VERSION: 21 jobs: build: - strategy: - matrix: - java: [ - 21 # Current Java LTS & minimum supported by Minecraft - ] - os: [ ubuntu-latest ] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout Repository uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v3 - - name: Gradle Cache + + - name: Setup Java + run: echo "JAVA_HOME=$JAVA_HOME_${{ env.JAVA_VERSION }}_X64" >> "$GITHUB_ENV" + + - name: Loom Cache uses: actions/cache@v4 with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - .gradle/loom-cache - build/ - key: ${{ runner.os }}-jdk${{ matrix.java }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle.properties', '**/gradle-wrapper.properties', '.github/workflows/build.yml') }} - - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + path: "**/.gradle/loom-cache" + key: "${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}" + restore-keys: "${{ runner.os }}-gradle-" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 with: - distribution: 'temurin' - java-version: ${{ matrix.java }} - - name: Make Gradle Wrapper Executable - if: ${{ runner.os != 'Windows' }} - run: chmod +x ./gradlew + gradle-home-cache-cleanup: true + cache-read-only: ${{ !endsWith(github.ref_name, '/dev') }} + + - name: Validate Gradle Wrapper Integrity + uses: gradle/wrapper-validation-action@v2 + - name: Build - # doesn't actually publish, as no secrets are passed in, just makes sure that publishing works - run: ./gradlew publish --no-daemon + # Doesn't actually publish, as no secrets are passed in, just makes sure that publishing works + # Also generate the mod jars for the test job + run: ./gradlew remapTestModJar publish --no-daemon + - name: Capture Build Artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '17' }} uses: actions/upload-artifact@v4 with: name: Artifacts path: | - common/build/libs/ - fabric/build/libs/ - forge/build/libs/ + common/build/libs/ + fabric/build/libs/ + forge/build/libs/ + + test: + strategy: + fail-fast: false + matrix: + loader: [ forge, fabric ] + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: Artifacts + + - name: Setup Environment Variables + run: | + echo "MOD_VERSION=$(grep '^mod_version =' gradle.properties | cut -d'=' -f2 | tr -d ' ')" >> "$GITHUB_ENV" + echo "MINECRAFT_VERSION=$(grep '^minecraft_version =' gradle.properties | cut -d'=' -f2 | tr -d ' ')" >> "$GITHUB_ENV" + echo "FABRIC_API_VERSION=$(grep '^fabric_api_version =' gradle.properties | cut -d'=' -f2 | tr -d ' ' | sed 's/+.*//')" >> "$GITHUB_ENV" + + - name: Move Test Mod and Flywheel into run/mods + run: | + mkdir -p run/mods + cp ${{ matrix.loader }}/build/libs/flywheel-${{ matrix.loader }}-${{ env.MINECRAFT_VERSION }}-${{ env.MOD_VERSION }}.jar run/mods + cp ${{ matrix.loader }}/build/libs/flywheel-${{ matrix.loader }}-${{ env.MINECRAFT_VERSION }}-${{ env.MOD_VERSION }}-testmod.jar run/mods + + # Lock to a specific commit, it would be bad if the tag is re-pushed with unwanted changes + - name: Run the MC client + uses: 3arthqu4ke/mc-runtime-test@e72f8fe1134aabf6fc749a2a8c09bb56dd7d283e + with: + mc: ${{ env.MINECRAFT_VERSION }} + modloader: ${{ matrix.loader }} + regex: .*${{ matrix.loader }}.* + mc-runtime-test: none + java: ${{ env.JAVA_VERSION }} + fabric-api: ${{ matrix.loader == 'fabric' && env.FABRIC_API_VERSION || 'none' }} diff --git a/buildSrc/src/main/kotlin/dev/engine_room/gradle/platform/PlatformExtension.kt b/buildSrc/src/main/kotlin/dev/engine_room/gradle/platform/PlatformExtension.kt index 66831cedf..2addef1dc 100644 --- a/buildSrc/src/main/kotlin/dev/engine_room/gradle/platform/PlatformExtension.kt +++ b/buildSrc/src/main/kotlin/dev/engine_room/gradle/platform/PlatformExtension.kt @@ -2,17 +2,17 @@ package dev.engine_room.gradle.platform import dev.engine_room.gradle.jarset.JarTaskSet import net.fabricmc.loom.api.LoomGradleExtensionAPI +import net.fabricmc.loom.task.RemapJarTask import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.named -import org.gradle.kotlin.dsl.provideDelegate -import org.gradle.kotlin.dsl.the -import org.gradle.kotlin.dsl.withType +import org.gradle.kotlin.dsl.* import org.gradle.language.jvm.tasks.ProcessResources +import java.io.File import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -102,6 +102,29 @@ open class PlatformExtension(val project: Project) { } } + fun setupTestMod(sourceSet: SourceSet) { + project.tasks.apply { + val testModJar = register("testModJar") { + from(sourceSet.output) + val file = File(project.layout.buildDirectory.asFile.get(), "devlibs"); + destinationDirectory.set(file) + archiveClassifier = "testmod" + } + + val remapTestModJar = register("remapTestModJar") { + dependsOn(testModJar) + inputFile.set(testModJar.get().archiveFile) + archiveClassifier = "testmod" + addNestedDependencies = false + classpath.from(sourceSet.compileClasspath) + } + + named("build").configure { + dependsOn(remapTestModJar) + } + } + } + private class DependentProject(private val thisProject: Project) : ReadWriteProperty { private var value: Project? = null diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/SourceChecks.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/SourceChecks.java deleted file mode 100644 index fdfc5c3d1..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/SourceChecks.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.engine_room.flywheel.backend.compile; - -// TODO: recycle to be invoked by the shader compiler -public class SourceChecks { - // public static final BiConsumer LAYOUT_VERTEX = checkFunctionArity("flw_layoutVertex", 0); - // public static final BiConsumer INSTANCE_VERTEX = checkFunctionParameterTypeExists("flw_instanceVertex", 1, 0); - // public static final BiConsumer MATERIAL_VERTEX = checkFunctionArity("flw_materialVertex", 0); - // public static final BiConsumer MATERIAL_FRAGMENT = checkFunctionArity("flw_materialFragment", 0); - // public static final BiConsumer CONTEXT_VERTEX = checkFunctionArity("flw_contextVertex", 0); - // public static final BiConsumer CONTEXT_FRAGMENT = checkFunctionArity("flw_contextFragment", 0).andThen(checkFunctionArity("flw_initFragment", 0)); - // public static final BiConsumer PIPELINE = checkFunctionArity("main", 0); - // - // public static BiConsumer checkFunctionArity(String name, int arity) { - // return (errorReporter, file) -> checkFunctionArity(errorReporter, file, name, arity); - // } - // - // public static BiConsumer checkFunctionParameterTypeExists(String name, int arity, int param) { - // return (errorReporter, file) -> { - // var func = checkFunctionArity(errorReporter, file, name, arity); - // - // if (func == null) { - // return; - // } - // - // var maybeStruct = func.getParameterType(param) - // .findStruct(); - // - // if (maybeStruct.isEmpty()) { - // errorReporter.generateMissingStruct(file, func.getParameterType(param), "struct not defined"); - // } - // }; - // } - // - // /** - // * @return {@code null} if the function doesn't exist, or if the function has the wrong arity. - // */ - // @Nullable - // private static ShaderFunction checkFunctionArity(ErrorReporter errorReporter, SourceFile file, String name, int arity) { - // Optional maybeFunc = file.findFunction(name); - // - // if (maybeFunc.isEmpty()) { - // errorReporter.generateMissingFunction(file, name, "\"" + name + "\" function not defined"); - // return null; - // } - // - // ShaderFunction func = maybeFunc.get(); - // ImmutableList params = func.getParameters(); - // if (params.size() != arity) { - // errorReporter.generateFunctionArgumentCountError(name, arity, func.getArgs()); - // return null; - // } - // - // return func; - // } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/InstanceAssemblerComponent.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/InstanceAssemblerComponent.java index 7115e1a8b..eb42189b4 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/InstanceAssemblerComponent.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/InstanceAssemblerComponent.java @@ -62,7 +62,7 @@ public abstract class InstanceAssemblerComponent implements SourceComponent { FLOAT_UNPACKING_FUNCS.put(FloatRepr.UNSIGNED_INT, e -> e.cast("float")); FLOAT_UNPACKING_FUNCS.put(FloatRepr.NORMALIZED_UNSIGNED_INT, e -> e.cast("float").div(4294967295f)); - FLOAT_UNPACKING_FUNCS.put(FloatRepr.FLOAT, e -> e.callFunction("uintBitsToFloat")); // FIXME: GLSL 330+ + FLOAT_UNPACKING_FUNCS.put(FloatRepr.FLOAT, e -> e.callFunction("uintBitsToFloat")); } protected final Layout layout; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java index 3215d12d2..605ee0441 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java @@ -1,27 +1,14 @@ package dev.engine_room.flywheel.backend.engine; -import java.util.ArrayList; - -import org.jetbrains.annotations.Nullable; - import dev.engine_room.flywheel.api.instance.Instance; import dev.engine_room.flywheel.api.instance.InstanceType; import dev.engine_room.flywheel.api.instance.Instancer; import dev.engine_room.flywheel.backend.engine.embed.Environment; -import dev.engine_room.flywheel.backend.util.AtomicBitSet; -public abstract class AbstractInstancer implements Instancer, InstanceHandleImpl.State { +public abstract class AbstractInstancer implements Instancer { public final InstanceType type; public final Environment environment; - private final Recreate recreate; - - // Lock for all instances, only needs to be used in methods that may run on the TaskExecutor. - protected final Object lock = new Object(); - protected final ArrayList instances = new ArrayList<>(); - protected final ArrayList> handles = new ArrayList<>(); - - protected final AtomicBitSet changed = new AtomicBitSet(); - protected final AtomicBitSet deleted = new AtomicBitSet(); + public final Recreate recreate; protected AbstractInstancer(InstancerKey key, Recreate recreate) { this.type = key.type(); @@ -29,218 +16,16 @@ protected AbstractInstancer(InstancerKey key, Recreate recreate) { this.recreate = recreate; } - @Override - public InstanceHandleImpl.State setChanged(int index) { - notifyDirty(index); - return this; - } - - @Override - public InstanceHandleImpl.State setDeleted(int index) { - notifyRemoval(index); - return InstanceHandleImpl.Deleted.instance(); - } - - @Override - public InstanceHandleImpl.State setVisible(InstanceHandleImpl handle, int index, boolean visible) { - if (visible) { - return this; - } - - notifyRemoval(index); - - I instance; - synchronized (lock) { - // I think we need to lock to prevent wacky stuff from happening if the array gets resized. - instance = instances.get(index); - } - - return new InstanceHandleImpl.Hidden<>(recreate, instance); - } - - @Override - public I createInstance() { - var handle = new InstanceHandleImpl<>(this); - I instance = type.create(handle); - - synchronized (lock) { - handle.index = instances.size(); - addLocked(instance, handle); - return instance; - } - } - - public void revealInstance(InstanceHandleImpl handle, I instance) { - synchronized (lock) { - handle.index = instances.size(); - addLocked(instance, handle); - } - } - - @Override - public void stealInstance(@Nullable I instance) { - if (instance == null) { - return; - } - - var instanceHandle = instance.handle(); - - if (!(instanceHandle instanceof InstanceHandleImpl)) { - // UB: do nothing - return; - } - - // Should InstanceType have an isInstance method? - @SuppressWarnings("unchecked") - var handle = (InstanceHandleImpl) instanceHandle; - - // No need to steal if this instance is already owned by this instancer. - if (handle.state == this) { - return; - } - // Not allowed to steal deleted instances. - if (handle.state instanceof InstanceHandleImpl.Deleted) { - return; - } - // No need to steal if the instance will recreate to us. - if (handle.state instanceof InstanceHandleImpl.Hidden hidden && recreate.equals(hidden.recreate())) { - return; - } - - // FIXME: in theory there could be a race condition here if the instance - // is somehow being stolen by 2 different instancers between threads. - // That seems kinda impossible so I'm fine leaving it as is for now. - - // Add the instance to this instancer. - if (handle.state instanceof AbstractInstancer other) { - // Remove the instance from its old instancer. - // This won't have any unwanted effect when the old instancer - // is filtering deleted instances later, so is safe. - other.notifyRemoval(handle.index); - - handle.state = this; - // Only lock now that we'll be mutating our state. - synchronized (lock) { - handle.index = instances.size(); - addLocked(instance, handle); - } - } else if (handle.state instanceof InstanceHandleImpl.Hidden) { - handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance); - } - } - - /** - * Calls must be synchronized on {@link #lock}. - */ - private void addLocked(I instance, InstanceHandleImpl handle) { - instances.add(instance); - handles.add(handle); - setIndexChanged(handle.index); - } - - public int instanceCount() { - return instances.size(); - } + public abstract InstanceHandleImpl.State revealInstance(InstanceHandleImpl handle, I instance); - public void notifyDirty(int index) { - if (index < 0 || index >= instanceCount()) { - return; - } - setIndexChanged(index); - } + public abstract int instanceCount(); - protected void setIndexChanged(int index) { - changed.set(index); - } - - public void notifyRemoval(int index) { - if (index < 0 || index >= instanceCount()) { - return; - } - deleted.set(index); - } - - public void removeDeletedInstances() { - if (deleted.isEmpty()) { - return; - } - - // Figure out which elements are to be removed. - final int oldSize = this.instances.size(); - int removeCount = deleted.cardinality(); - - if (oldSize == removeCount) { - clear(); - return; - } - - final int newSize = oldSize - removeCount; - - // Start from the first deleted index. - int writePos = deleted.nextSetBit(0); - - if (writePos < newSize) { - // Since we'll be shifting everything into this space we can consider it all changed. - setRangeChanged(writePos, newSize); - } - - // We definitely shouldn't consider the deleted instances as changed though, - // else we might try some out of bounds accesses later. - changed.clear(newSize, oldSize); - - // Punch out the deleted instances, shifting over surviving instances to fill their place. - for (int scanPos = writePos; (scanPos < oldSize) && (writePos < newSize); scanPos++, writePos++) { - // Find next non-deleted element. - scanPos = deleted.nextClearBit(scanPos); - - if (scanPos != writePos) { - // Grab the old instance/handle from scanPos... - var handle = handles.get(scanPos); - I instance = instances.get(scanPos); - - // ... and move it to writePos. - handles.set(writePos, handle); - instances.set(writePos, instance); - - // Make sure the handle knows it's been moved - handle.index = writePos; - } - } - - deleted.clear(); - instances.subList(newSize, oldSize) - .clear(); - handles.subList(newSize, oldSize) - .clear(); - } - - protected void setRangeChanged(int start, int end) { - changed.set(start, end); - } - - /** - * Clear all instances without freeing resources. - */ - public void clear() { - for (InstanceHandleImpl handle : handles) { - // Only clear instances that belong to this instancer. - // If one of these handles was stolen by another instancer, - // clearing it here would cause significant visual artifacts and instance leaks. - // At the same time, we need to clear handles we own to prevent - // instances from changing/deleting positions in this instancer that no longer exist. - if (handle.state == this) { - handle.clear(); - handle.state = InstanceHandleImpl.Deleted.instance(); - } - } - instances.clear(); - handles.clear(); - changed.clear(); - deleted.clear(); - } + public abstract void parallelUpdate(); public abstract void delete(); + public abstract void clear(); + @Override public String toString() { return "AbstractInstancer[" + instanceCount() + ']'; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/BaseInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/BaseInstancer.java new file mode 100644 index 000000000..eae6d73cb --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/BaseInstancer.java @@ -0,0 +1,175 @@ +package dev.engine_room.flywheel.backend.engine; + +import java.util.ArrayList; + +import org.jetbrains.annotations.Nullable; + +import dev.engine_room.flywheel.api.instance.Instance; +import dev.engine_room.flywheel.backend.util.AtomicBitSet; + +public abstract class BaseInstancer extends AbstractInstancer implements InstanceHandleImpl.State { + // Lock for all instances, only needs to be used in methods that may run on the TaskExecutor. + protected final Object lock = new Object(); + protected final ArrayList instances = new ArrayList<>(); + protected final ArrayList> handles = new ArrayList<>(); + + protected final AtomicBitSet changed = new AtomicBitSet(); + protected final AtomicBitSet deleted = new AtomicBitSet(); + + protected BaseInstancer(InstancerKey key, Recreate recreate) { + super(key, recreate); + } + + + @Override + public InstanceHandleImpl.State setChanged(int index) { + notifyDirty(index); + return this; + } + + @Override + public InstanceHandleImpl.State setDeleted(int index) { + notifyRemoval(index); + return InstanceHandleImpl.Deleted.instance(); + } + + @Override + public InstanceHandleImpl.State setVisible(InstanceHandleImpl handle, int index, boolean visible) { + if (visible) { + return this; + } + + notifyRemoval(index); + + I instance; + synchronized (lock) { + // I think we need to lock to prevent wacky stuff from happening if the array gets resized. + instance = instances.get(index); + } + + return new InstanceHandleImpl.Hidden<>(recreate, instance); + } + + @Override + public I createInstance() { + var handle = new InstanceHandleImpl<>(this); + I instance = type.create(handle); + + synchronized (lock) { + handle.index = instances.size(); + addLocked(instance, handle); + return instance; + } + } + + public InstanceHandleImpl.State revealInstance(InstanceHandleImpl handle, I instance) { + synchronized (lock) { + handle.index = instances.size(); + addLocked(instance, handle); + } + return this; + } + + @Override + public void stealInstance(@Nullable I instance) { + if (instance == null) { + return; + } + + var instanceHandle = instance.handle(); + + if (!(instanceHandle instanceof InstanceHandleImpl)) { + // UB: do nothing + return; + } + + // Should InstanceType have an isInstance method? + @SuppressWarnings("unchecked") var handle = (InstanceHandleImpl) instanceHandle; + + // No need to steal if this instance is already owned by this instancer. + if (handle.state == this) { + return; + } + // Not allowed to steal deleted instances. + if (handle.state instanceof InstanceHandleImpl.Deleted) { + return; + } + // No need to steal if the instance will recreate to us. + if (handle.state instanceof InstanceHandleImpl.Hidden hidden && recreate.equals(hidden.recreate())) { + return; + } + + // FIXME: in theory there could be a race condition here if the instance + // is somehow being stolen by 2 different instancers between threads. + // That seems kinda impossible so I'm fine leaving it as is for now. + + // Add the instance to this instancer. + if (handle.state instanceof BaseInstancer other) { + // Remove the instance from its old instancer. + // This won't have any unwanted effect when the old instancer + // is filtering deleted instances later, so is safe. + other.notifyRemoval(handle.index); + + handle.state = this; + // Only lock now that we'll be mutating our state. + synchronized (lock) { + handle.index = instances.size(); + addLocked(instance, handle); + } + } else if (handle.state instanceof InstanceHandleImpl.Hidden) { + handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance); + } + } + + /** + * Calls must be synchronized on {@link #lock}. + */ + private void addLocked(I instance, InstanceHandleImpl handle) { + instances.add(instance); + handles.add(handle); + setIndexChanged(handle.index); + } + + public int instanceCount() { + return instances.size(); + } + + public void notifyDirty(int index) { + if (index < 0 || index >= instanceCount()) { + return; + } + setIndexChanged(index); + } + + protected void setIndexChanged(int index) { + changed.set(index); + } + + public void notifyRemoval(int index) { + if (index < 0 || index >= instanceCount()) { + return; + } + deleted.set(index); + } + + /** + * Clear all instances without freeing resources. + */ + public void clear() { + for (InstanceHandleImpl handle : handles) { + // Only clear instances that belong to this instancer. + // If one of these handles was stolen by another instancer, + // clearing it here would cause significant visual artifacts and instance leaks. + // At the same time, we need to clear handles we own to prevent + // instances from changing/deleting positions in this instancer that no longer exist. + if (handle.state == this) { + handle.clear(); + handle.state = InstanceHandleImpl.Deleted.instance(); + } + } + instances.clear(); + handles.clear(); + changed.clear(); + deleted.clear(); + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java index f34887f00..9f5c4e2ed 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java @@ -8,6 +8,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import org.jetbrains.annotations.Nullable; + import com.mojang.datafixers.util.Pair; import dev.engine_room.flywheel.api.RenderContext; @@ -26,6 +28,8 @@ import net.minecraft.client.resources.model.ModelBakery; public abstract class DrawManager> { + private static final boolean WARN_EMPTY_MODELS = Boolean.getBoolean("flywheel.warnEmptyModels"); + /** * A map of instancer keys to instancers. *
@@ -50,14 +54,19 @@ public AbstractInstancer getInstancer(InstancerKey ke public Plan createFramePlan() { // Go wide on instancers to process deletions in parallel. - return ForEachPlan.of(() -> new ArrayList<>(instancers.values()), AbstractInstancer::removeDeletedInstances); + return ForEachPlan.of(() -> new ArrayList<>(instancers.values()), AbstractInstancer::parallelUpdate); } public void flush(LightStorage lightStorage, EnvironmentStorage environmentStorage) { // Thread safety: flush is called from the render thread after all visual updates have been made, // so there are no:tm: threads we could be racing with. - for (var instancer : initializationQueue) { - initialize(instancer.key(), instancer.instancer()); + for (var init : initializationQueue) { + var instancer = init.instancer(); + if (instancer.instanceCount() > 0) { + initialize(init.key(), instancer); + } else { + instancers.remove(init.key()); + } } initializationQueue.clear(); } @@ -93,20 +102,27 @@ private static boolean checkAndWarnEmptyModel(Model model) { return true; } - StringBuilder builder = new StringBuilder(); - builder.append("Creating an instancer for a model with no meshes! Stack trace:"); + if (WARN_EMPTY_MODELS) { + StringBuilder builder = new StringBuilder(); + builder.append("Creating an instancer for a model with no meshes! Stack trace:"); - StackWalker.getInstance() - // .walk(s -> s.skip(3)) // this causes forEach to crash for some reason - .forEach(f -> builder.append("\n\t") - .append(f.toString())); + StackWalker.getInstance() + .forEach(f -> builder.append("\n\t") + .append(f.toString())); - FlwBackend.LOGGER.warn(builder.toString()); + FlwBackend.LOGGER.warn(builder.toString()); + } return false; } - protected static > Map, Int2ObjectMap>>>> doCrumblingSort(Class clazz, List crumblingBlocks) { + @FunctionalInterface + protected interface State2Instancer> { + // I tried using a plain Function, I> here, but it exploded with type errors. + @Nullable I apply(InstanceHandleImpl.State state); + } + + protected static > Map, Int2ObjectMap>>>> doCrumblingSort(List crumblingBlocks, State2Instancer cast) { Map, Int2ObjectMap>>>> byType = new HashMap<>(); for (Engine.CrumblingBlock block : crumblingBlocks) { int progress = block.progress(); @@ -123,16 +139,12 @@ protected static > Map, Int2ObjectMap continue; } - InstanceHandleImpl.State abstractInstancer = impl.state; - // AbstractInstancer directly implement HandleState, so this check is valid. - if (!clazz.isInstance(abstractInstancer)) { - // This rejects instances that were created by a different engine, - // and also instances that are hidden or deleted. + var instancer = cast.apply(impl.state); + + if (instancer == null) { continue; } - var instancer = clazz.cast(abstractInstancer); - byType.computeIfAbsent(new GroupKey<>(instancer.type, instancer.environment), $ -> new Int2ObjectArrayMap<>()) .computeIfAbsent(progress, $ -> new ArrayList<>()) .add(Pair.of(instancer, impl)); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/InstanceHandleImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/InstanceHandleImpl.java index d5932f45e..e83936b0c 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/InstanceHandleImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/InstanceHandleImpl.java @@ -1,13 +1,16 @@ package dev.engine_room.flywheel.backend.engine; +import org.jetbrains.annotations.UnknownNullability; + import dev.engine_room.flywheel.api.instance.Instance; import dev.engine_room.flywheel.api.instance.InstanceHandle; public class InstanceHandleImpl implements InstanceHandle { + @UnknownNullability public State state; public int index; - public InstanceHandleImpl(State state) { + public InstanceHandleImpl(@UnknownNullability State state) { this.state = state; } @@ -62,8 +65,7 @@ public State setVisible(InstanceHandleImpl handle, int index, boolean visi return this; } var instancer = recreate.recreate(); - instancer.revealInstance(handle, instance); - return instancer; + return instancer.revealInstance(handle, instance); } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java index bb04c7760..885c616dd 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java @@ -1,142 +1,212 @@ package dev.engine_room.flywheel.backend.engine; -import org.jetbrains.annotations.NotNull; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntObjectImmutablePair; -import it.unimi.dsi.fastutil.ints.IntObjectPair; -import it.unimi.dsi.fastutil.longs.Long2IntMap; -import it.unimi.dsi.fastutil.longs.LongArrayList; -import it.unimi.dsi.fastutil.longs.LongComparator; -import it.unimi.dsi.fastutil.objects.ReferenceArrayList; import net.minecraft.core.SectionPos; +// Massive kudos to RogueLogix for figuring out this LUT scheme. +// First layer is Y, then X, then Z. public final class LightLut { - private static final LongComparator SECTION_X_THEN_Y_THEN_Z = (long a, long b) -> { - final var xComp = Integer.compare(SectionPos.x(a), SectionPos.x(b)); - if (xComp != 0) { - return xComp; + private final Layer> indices = new Layer<>(); + + public void add(long position, int index) { + final var x = SectionPos.x(position); + final var y = SectionPos.y(position); + final var z = SectionPos.z(position); + + indices.computeIfAbsent(y, Layer::new) + .computeIfAbsent(x, IntLayer::new) + .set(z, index + 1); + } + + public void remove(long section) { + final var x = SectionPos.x(section); + final var y = SectionPos.y(section); + final var z = SectionPos.z(section); + + var first = indices.get(y); + + if (first == null) { + return; } - var yComp = Integer.compare(SectionPos.y(a), SectionPos.y(b)); - if (yComp != 0) { - return yComp; + + var second = first.get(x); + + if (second == null) { + return; } - return Integer.compare(SectionPos.z(a), SectionPos.z(b)); - }; - private LightLut() { + second.clear(z); } - // Massive kudos to RogueLogix for figuring out this LUT scheme. - // TODO: switch to y x z or x z y ordering - // DATA LAYOUT - // [0] : base chunk X, X index count, followed by linear indices of y blocks - // [yBlockIndex] : baseChunk Y, Y index count, followed by linear indices of z blocks for this x - // [zBlockIndex] : baseChunk Z, Z index count, followed by linear indices of lighting chunks - // this data layout allows a single buffer to represent the lighting volume, without requiring the entire 3d lookup volume to be allocated - public static IntArrayList buildLut(Long2IntMap sectionIndicesMaps) { - if (sectionIndicesMaps.isEmpty()) { - return new IntArrayList(); + public IntArrayList flatten() { + final var out = new IntArrayList(); + indices.fillLut(out, (yIndices, lut) -> yIndices.fillLut(lut, IntLayer::fillLut)); + return out; + } + + private static final class Layer { + private boolean hasBase = false; + private int base = 0; + private Object[] nextLayer = new Object[0]; + + public void fillLut(IntArrayList lut, BiConsumer inner) { + lut.add(base); + lut.add(nextLayer.length); + + int innerIndexBase = lut.size(); + + // Reserve space for the inner indices... + lut.size(innerIndexBase + nextLayer.length); + + for (int i = 0; i < nextLayer.length; i++) { + final var innerIndices = (T) nextLayer[i]; + if (innerIndices == null) { + continue; + } + + int layerPosition = lut.size(); + + // ...so we can write in their actual positions later. + lut.set(innerIndexBase + i, layerPosition); + + // Append the next layer to the lut. + inner.accept(innerIndices, lut); + } } - final var positions = sortedKeys(sectionIndicesMaps); - final var baseX = SectionPos.x(positions.getLong(0)); - return buildLut(baseX, buildIndices(sectionIndicesMaps, positions, baseX)); - } + @Nullable + public T get(int i) { + if (!hasBase) { + return null; + } - private static ReferenceArrayList>> buildIndices(Long2IntMap sectionIndicesMaps, LongArrayList positions, int baseX) { - final var indices = new ReferenceArrayList>>(); - for (long position : positions) { - final var x = SectionPos.x(position); - final var y = SectionPos.y(position); - final var z = SectionPos.z(position); - - final var xIndex = x - baseX; - if (indices.size() <= xIndex) { - indices.ensureCapacity(xIndex + 1); - indices.size(xIndex + 1); + if (i < base) { + return null; } - var yLookup = indices.get(xIndex); - if (yLookup == null) { - //noinspection SuspiciousNameCombination - yLookup = new IntObjectImmutablePair<>(y, new ReferenceArrayList<>()); - indices.set(xIndex, yLookup); + + final var offset = i - base; + + if (offset >= nextLayer.length) { + return null; } - final var yIndices = yLookup.right(); - final var yIndex = y - yLookup.leftInt(); - if (yIndices.size() <= yIndex) { - yIndices.ensureCapacity(yIndex + 1); - yIndices.size(yIndex + 1); + return (T) nextLayer[offset]; + } + + public T computeIfAbsent(int i, Supplier ifAbsent) { + if (!hasBase) { + // We don't want to default to base 0, so we'll use the first value we get. + base = i; + hasBase = true; } - var zLookup = yIndices.get(yIndex); - if (zLookup == null) { - zLookup = new IntArrayList(); - zLookup.add(z); - zLookup.add(0); // this value will be filled in later - yIndices.set(yIndex, zLookup); + + if (i < base) { + rebase(i); } - final var zIndex = z - zLookup.getInt(0); - if ((zLookup.size() - 2) <= zIndex) { - zLookup.ensureCapacity(zIndex + 3); - zLookup.size(zIndex + 3); + final var offset = i - base; + + if (offset >= nextLayer.length) { + resize(offset + 1); } - // Add 1 to the actual index so that 0 indicates a missing section. - zLookup.set(zIndex + 2, sectionIndicesMaps.get(position) + 1); + + var out = nextLayer[offset]; + + if (out == null) { + out = ifAbsent.get(); + nextLayer[offset] = out; + } + return (T) out; } - return indices; - } - private static @NotNull LongArrayList sortedKeys(Long2IntMap sectionIndicesMaps) { - final var out = new LongArrayList(sectionIndicesMaps.keySet()); - out.unstableSort(SECTION_X_THEN_Y_THEN_Z); - return out; + private void resize(int length) { + final var newIndices = new Object[length]; + System.arraycopy(nextLayer, 0, newIndices, 0, nextLayer.length); + nextLayer = newIndices; + } + + private void rebase(int newBase) { + final var growth = base - newBase; + + final var newIndices = new Object[nextLayer.length + growth]; + // Shift the existing elements to the end of the new array to maintain their offset with the new base. + System.arraycopy(nextLayer, 0, newIndices, growth, nextLayer.length); + + nextLayer = newIndices; + base = newBase; + } } - private static IntArrayList buildLut(int baseX, ReferenceArrayList>> indices) { - final var out = new IntArrayList(); - out.add(baseX); - out.add(indices.size()); - for (int i = 0; i < indices.size(); i++) { - out.add(0); + private static final class IntLayer { + private boolean hasBase = false; + private int base = 0; + private int[] indices = new int[0]; + + public void fillLut(IntArrayList lut) { + lut.add(base); + lut.add(indices.length); + + for (int index : indices) { + lut.add(index); + } } - for (int x = 0; x < indices.size(); x++) { - final var yLookup = indices.get(x); - if (yLookup == null) { - out.set(x + 2, 0); - continue; + + public void set(int i, int index) { + if (!hasBase) { + base = i; + hasBase = true; } - // ensure that the base position and size dont cross a (64 byte) cache line - if ((out.size() & 0xF) == 0xF) { - out.add(0); + + if (i < base) { + rebase(i); } - final var baseYIndex = out.size(); - out.set(x + 2, baseYIndex); + final var offset = i - base; - final var yIndices = yLookup.right(); - out.add(yLookup.leftInt()); - out.add(yIndices.size()); - for (int i = 0; i < indices.size(); i++) { - out.add(0); + if (offset >= indices.length) { + resize(offset + 1); } - for (int y = 0; y < yIndices.size(); y++) { - final var zLookup = yIndices.get(y); - if (zLookup == null) { - out.set(baseYIndex + y + 2, 0); - continue; - } - // ensure that the base position and size dont cross a (64 byte) cache line - if ((out.size() & 0xF) == 0xF) { - out.add(0); - } - out.set(baseYIndex + y + 2, out.size()); - zLookup.set(1, zLookup.size() - 2); - out.addAll(zLookup); + indices[offset] = index; + } + + public void clear(int i) { + if (!hasBase) { + return; + } + + if (i < base) { + return; + } + + final var offset = i - base; + + if (offset >= indices.length) { + return; } + + indices[offset] = 0; + } + + private void resize(int length) { + final var newIndices = new int[length]; + System.arraycopy(indices, 0, newIndices, 0, indices.length); + indices = newIndices; + } + + private void rebase(int newBase) { + final var growth = base - newBase; + + final var newIndices = new int[indices.length + growth]; + System.arraycopy(indices, 0, newIndices, growth, indices.length); + + indices = newIndices; + base = newBase; } - return out; } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java index 47e884a52..f8b91be89 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java @@ -22,7 +22,6 @@ import net.minecraft.world.level.lighting.LayerLightEventListener; /** - * TODO: AO data * A managed arena of light sections for uploading to the GPU. * *

Each section represents an 18x18x18 block volume of light data. @@ -45,12 +44,9 @@ public class LightStorage { private static final int INVALID_SECTION = -1; private final LevelAccessor level; - + private final LightLut lut; private final CpuArena arena; - private final Long2IntMap section2ArenaIndex = new Long2IntOpenHashMap(); - { - section2ArenaIndex.defaultReturnValue(INVALID_SECTION); - } + private final Long2IntMap section2ArenaIndex; private final BitSet changed = new BitSet(); private boolean needsLutRebuild = false; @@ -61,8 +57,10 @@ public class LightStorage { public LightStorage(LevelAccessor level) { this.level = level; - + lut = new LightLut(); arena = new CpuArena(SECTION_SIZE_BYTES, DEFAULT_ARENA_CAPACITY_SECTIONS); + section2ArenaIndex = new Long2IntOpenHashMap(); + section2ArenaIndex.defaultReturnValue(INVALID_SECTION); } /** @@ -116,7 +114,6 @@ public Plan createFramePlan() { } // Now actually do the collection. - // TODO: Should this be done in parallel? sectionsToCollect.forEach(this::collectSection); updatedSections.clear(); @@ -137,12 +134,22 @@ private void removeUnusedSections() { if (!requestedSections.contains(section)) { arena.free(entry.getIntValue()); - needsLutRebuild = true; + endTrackingSection(section); it.remove(); } } } + private void beginTrackingSection(long section, int index) { + lut.add(section, index); + needsLutRebuild = true; + } + + private void endTrackingSection(long section) { + lut.remove(section); + needsLutRebuild = true; + } + public int capacity() { return arena.capacity(); } @@ -376,7 +383,7 @@ private int indexForSection(long section) { if (out == INVALID_SECTION) { out = arena.alloc(); section2ArenaIndex.put(section, out); - needsLutRebuild = true; + beginTrackingSection(section, out); } return out; } @@ -408,8 +415,7 @@ public void upload(GlBuffer buffer) { } public IntArrayList createLut() { - // TODO: incremental lut updates - return LightLut.buildLut(section2ArenaIndex); + return lut.flatten(); } private enum SectionEdge { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MeshPool.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MeshPool.java index 5a41c1068..6cfa31424 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MeshPool.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MeshPool.java @@ -70,23 +70,17 @@ public void flush() { if (anyToRemove) { anyToRemove = false; processDeletions(); + } - // Might want to shrink the index pool if something was removed. - indexPool.reset(); - for (PooledMesh mesh : meshList) { - indexPool.updateCount(mesh.mesh.indexSequence(), mesh.indexCount()); - } - } else { + if (!recentlyAllocated.isEmpty()) { // Otherwise, just update the index with the new counts. for (PooledMesh mesh : recentlyAllocated) { indexPool.updateCount(mesh.mesh.indexSequence(), mesh.indexCount()); } + indexPool.flush(); recentlyAllocated.clear(); } - // Always need to flush the index pool. - indexPool.flush(); - uploadAll(); dirty = false; } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java index 4b14df6b2..65c0d4aba 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java @@ -186,10 +186,14 @@ public void delete() { programs.release(); depthPyramid.delete(); + + lightBuffers.delete(); + + matrixBuffer.delete(); } public void renderCrumbling(List crumblingBlocks) { - var byType = doCrumblingSort(IndirectInstancer.class, crumblingBlocks); + var byType = doCrumblingSort(crumblingBlocks, IndirectInstancer::fromState); if (byType.isEmpty()) { return; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java index ee11999a8..1291e9a77 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java @@ -2,7 +2,10 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.joml.Vector4fc; import org.lwjgl.system.MemoryUtil; @@ -10,6 +13,7 @@ import dev.engine_room.flywheel.api.instance.Instance; import dev.engine_room.flywheel.api.instance.InstanceWriter; import dev.engine_room.flywheel.backend.engine.AbstractInstancer; +import dev.engine_room.flywheel.backend.engine.InstanceHandleImpl; import dev.engine_room.flywheel.backend.engine.InstancerKey; import dev.engine_room.flywheel.backend.util.AtomicBitSet; import dev.engine_room.flywheel.lib.math.MoreMath; @@ -20,7 +24,27 @@ public class IndirectInstancer extends AbstractInstancer private final List associatedDraws = new ArrayList<>(); private final Vector4fc boundingSphere; - private final AtomicBitSet changedPages = new AtomicBitSet(); + private final AtomicReference[]> pages = new AtomicReference<>(pageArray(0)); + /** + * The set of pages whose count changed and thus need their descriptor re-uploaded. + */ + private final AtomicBitSet validityChanged = new AtomicBitSet(); + /** + * The set of pages whose content changed and thus need their instances re-uploaded. + * Note that we don't re-upload for deletions, as the memory becomes invalid and masked out by the validity bits. + */ + private final AtomicBitSet contentsChanged = new AtomicBitSet(); + /** + * The set of pages that are entirely full. + * We scan the clear bits of this set when trying to add an instance. + */ + private final AtomicBitSet fullPages = new AtomicBitSet(); + /** + * The set of mergable pages. A page is mergeable if it is not empty and has 16 or fewer instances. + * These constraints are set so that we can guarantee that merging two pages leaves one entirely empty, + * but we also don't want to waste work merging into pages that are already empty. + */ + private final AtomicBitSet mergeablePages = new AtomicBitSet(); public ObjectStorage.@UnknownNullability Mapping mapping; @@ -35,16 +59,216 @@ public IndirectInstancer(InstancerKey key, Recreate recreate) { boundingSphere = key.model().boundingSphere(); } - @Override - public void setIndexChanged(int index) { - changedPages.set(ObjectStorage.objectIndex2PageIndex(index)); + @SuppressWarnings("unchecked") + private static InstancePage[] pageArray(int length) { + return new InstancePage[length]; } - @Override - protected void setRangeChanged(int start, int end) { - super.setRangeChanged(start, end); + @SuppressWarnings("unchecked") + private static I[] instanceArray() { + return (I[]) new Instance[ObjectStorage.PAGE_SIZE]; + } + + @SuppressWarnings("unchecked") + private static InstanceHandleImpl[] handleArray() { + return new InstanceHandleImpl[ObjectStorage.PAGE_SIZE]; + } + + @Nullable + public static IndirectInstancer fromState(InstanceHandleImpl.State handle) { + if (handle instanceof InstancePage instancer) { + return instancer.parent; + } + return null; + } + + private static final class InstancePage implements InstanceHandleImpl.State { + private final IndirectInstancer parent; + private final int pageNo; + private final I[] instances; + // Handles are only read in #takeFrom. It would be nice to avoid tracking these at all. + private final InstanceHandleImpl[] handles; + /** + * A bitset describing which indices in the instances/handles arrays contain live instances. + */ + private final AtomicInteger valid; + + private InstancePage(IndirectInstancer parent, int pageNo) { + this.parent = parent; + this.pageNo = pageNo; + this.instances = instanceArray(); + this.handles = handleArray(); + this.valid = new AtomicInteger(0); + } + + /** + * Attempt to add the given instance/handle to this page. + * + * @param instance The instance to add + * @param handle The instance's handle + * @return true if the instance was added, false if the page is full + */ + public boolean add(I instance, InstanceHandleImpl handle) { + // Thread safety: we loop until we either win the race and add the given instance, or we + // run out of space because other threads trying to add at the same time. + while (true) { + int currentValue = valid.get(); + if (isFull(currentValue)) { + // The page is full, must search elsewhere + return false; + } + + // determine what the new long value will be after we set the appropriate bit. + int index = Integer.numberOfTrailingZeros(~currentValue); + + int newValue = currentValue | (1 << index); + + // if no other thread has modified the value since we read it, we won the race and we are done. + if (valid.compareAndSet(currentValue, newValue)) { + instances[index] = instance; + handles[index] = handle; + handle.state = this; + // Handle index is unique amongst all pages of this instancer. + handle.index = local2HandleIndex(index); + + parent.contentsChanged.set(pageNo); + parent.validityChanged.set(pageNo); + if (isFull(newValue)) { + // The page is now full, mark it so in the bitset. + // This is safe because only one bit position changes at a time. + parent.fullPages.set(pageNo); + } + if (isEmpty(currentValue)) { + // Value we just saw was zero, so since we added something we are now mergeable! + parent.mergeablePages.set(pageNo); + } else if (Integer.bitCount(currentValue) == 16) { + // We just filled the 17th instance, so we are no longer mergeable. + parent.mergeablePages.clear(pageNo); + } + return true; + } + } + } + + private int local2HandleIndex(int index) { + return (pageNo << ObjectStorage.LOG_2_PAGE_SIZE) + index; + } + + @Override + public InstanceHandleImpl.State setChanged(int index) { + parent.contentsChanged.set(pageNo); + return this; + } + + @Override + public InstanceHandleImpl.State setDeleted(int index) { + int localIndex = index % ObjectStorage.PAGE_SIZE; + + clear(localIndex); + + return InstanceHandleImpl.Deleted.instance(); + } + + @Override + public InstanceHandleImpl.State setVisible(InstanceHandleImpl handle, int index, boolean visible) { + if (visible) { + return this; + } + + int localIndex = index % ObjectStorage.PAGE_SIZE; + + var out = instances[localIndex]; + + clear(localIndex); + + return new InstanceHandleImpl.Hidden<>(parent.recreate, out); + } + + private void clear(int localIndex) { + instances[localIndex] = null; + handles[localIndex] = null; + + while (true) { + int currentValue = valid.get(); + int newValue = currentValue & ~(1 << localIndex); + + if (valid.compareAndSet(currentValue, newValue)) { + parent.validityChanged.set(pageNo); + if (isEmpty(newValue)) { + // If we decremented to zero then we're no longer mergeable. + parent.mergeablePages.clear(pageNo); + } else if (Integer.bitCount(newValue) == 16) { + // If we decremented to 16 then we're now mergeable. + parent.mergeablePages.set(pageNo); + } + // Set full page last so that other threads don't race to set the other bitsets. + parent.fullPages.clear(pageNo); + break; + } + } + } + + /** + * Only call this on 2 pages that are mergeable. + * + * @param other The page to take instances from. + */ + private void takeFrom(InstancePage other) { + // Fill the holes in this page with instances from the other page. + + int valid = this.valid.get(); + int otherValid = other.valid.get(); + + for (int i = 0; i < ObjectStorage.PAGE_SIZE; i++) { + int mask = 1 << i; + + // Find set bits in the other page. + if ((otherValid & mask) == 0) { + continue; + } + + int writePos = Integer.numberOfTrailingZeros(~valid); + + instances[writePos] = other.instances[i]; + handles[writePos] = other.handles[i]; + + handles[writePos].state = this; + handles[writePos].index = local2HandleIndex(writePos); + + // Clear out the other page. + otherValid &= ~mask; + other.handles[i] = null; + other.instances[i] = null; + + // Set the bit in this page so we can find the next write position. + valid |= 1 << writePos; + + // If we're full, we're done. + if (isFull(valid)) { + break; + } + } + + this.valid.set(valid); + other.valid.set(otherValid); + + // If the other page was quite empty we may still be mergeable. + parent.mergeablePages.set(pageNo, isMergeable(valid)); - changedPages.set(ObjectStorage.objectIndex2PageIndex(start), ObjectStorage.objectIndex2PageIndex(end) + 1); + // We definitely changed the contents and validity of this page. + parent.contentsChanged.set(pageNo); + parent.validityChanged.set(pageNo); + + // The other page will end up empty, so the validity changes and it's no longer mergeable. + // Also clear the changed bit so we don't re-upload the instances. + parent.contentsChanged.clear(other.pageNo); + parent.validityChanged.set(other.pageNo); + parent.mergeablePages.clear(other.pageNo); + + if (isFull(valid)) { + parent.fullPages.set(pageNo); + } + } } public void addDraw(IndirectDraw draw) { @@ -56,9 +280,32 @@ public List draws() { } public void update(int modelIndex, int baseInstance) { - this.modelIndex = modelIndex; this.baseInstance = baseInstance; - mapping.update(modelIndex, instanceCount()); + + var sameModelIndex = this.modelIndex == modelIndex; + if (sameModelIndex && validityChanged.isEmpty()) { + // Nothing to do! + return; + } + + this.modelIndex = modelIndex; + + var pages = this.pages.get(); + mapping.updateCount(pages.length); + + if (sameModelIndex) { + // Only need to update the changed pages. + for (int page = validityChanged.nextSetBit(0); page >= 0 && page < pages.length; page = validityChanged.nextSetBit(page + 1)) { + mapping.updatePage(page, modelIndex, pages[page].valid.get()); + } + } else { + // Need to update all pages since the model index changed. + for (int i = 0; i < pages.length; i++) { + mapping.updatePage(i, modelIndex, pages[i].valid.get()); + } + } + + validityChanged.clear(); } public void writeModel(long ptr) { @@ -72,25 +319,22 @@ public void writeModel(long ptr) { } public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) { - if (changedPages.isEmpty()) { + if (contentsChanged.isEmpty()) { return; } - int numPages = mapping.pageCount(); + var pages = this.pages.get(); + for (int page = contentsChanged.nextSetBit(0); page >= 0 && page < pages.length; page = contentsChanged.nextSetBit(page + 1)) { + var instances = pages[page].instances; - var instanceCount = instances.size(); - - for (int page = changedPages.nextSetBit(0); page >= 0 && page < numPages; page = changedPages.nextSetBit(page + 1)) { - int startObject = ObjectStorage.pageIndex2ObjectIndex(page); + long baseByte = mapping.page2ByteOffset(page); - if (startObject >= instanceCount) { - break; + if (baseByte < 0) { + // This page is not mapped to the VBO. + continue; } - int endObject = Math.min(instanceCount, ObjectStorage.pageIndex2ObjectIndex(page + 1)); - - long baseByte = mapping.page2ByteOffset(page); - long size = (endObject - startObject) * instanceStride; + long size = ObjectStorage.PAGE_SIZE * instanceStride; // Because writes are broken into pages, we end up with significantly more calls into // StagingBuffer#enqueueCopy and the allocations for the writer got out of hand. Here @@ -101,9 +345,10 @@ public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) { long direct = stagingBuffer.reserveForCopy(size, instanceVbo, baseByte); if (direct != MemoryUtil.NULL) { - for (int i = startObject; i < endObject; i++) { - var instance = instances.get(i); - writer.write(direct, instance); + for (I instance : instances) { + if (instance != null) { + writer.write(direct, instance); + } direct += instanceStride; } continue; @@ -112,15 +357,49 @@ public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) { // Otherwise, write to a scratch buffer and enqueue a copy. var block = stagingBuffer.getScratch(size); var ptr = block.ptr(); - for (int i = startObject; i < endObject; i++) { - var instance = instances.get(i); - writer.write(ptr, instance); + for (I instance : instances) { + if (instance != null) { + writer.write(ptr, instance); + } ptr += instanceStride; } stagingBuffer.enqueueCopy(block.ptr(), size, instanceVbo, baseByte); } - changedPages.clear(); + contentsChanged.clear(); + } + + public void parallelUpdate() { + var pages = this.pages.get(); + + int page = 0; + while (mergeablePages.cardinality() > 1) { + page = mergeablePages.nextSetBit(page); + if (page < 0) { + break; + } + + // Find the next mergeable page. + int next = mergeablePages.nextSetBit(page + 1); + if (next < 0) { + break; + } + + // Try to merge the pages. + pages[page].takeFrom(pages[next]); + } + } + + private static boolean isFull(int valid) { + return valid == 0xFFFFFFFF; + } + + private static boolean isEmpty(int valid) { + return valid == 0; + } + + private static boolean isMergeable(int valid) { + return !isEmpty(valid) && Integer.bitCount(valid) <= 16; } @Override @@ -143,4 +422,135 @@ public int baseInstance() { public int local2GlobalInstanceIndex(int instanceIndex) { return mapping.objectIndex2GlobalIndex(instanceIndex); } + + @Override + public I createInstance() { + var handle = new InstanceHandleImpl(null); + I instance = type.create(handle); + + addInner(instance, handle); + + return instance; + } + + public InstanceHandleImpl.State revealInstance(InstanceHandleImpl handle, I instance) { + addInner(instance, handle); + return handle.state; + } + + @Override + public void stealInstance(@Nullable I instance) { + if (instance == null) { + return; + } + + var instanceHandle = instance.handle(); + + if (!(instanceHandle instanceof InstanceHandleImpl)) { + // UB: do nothing + return; + } + + // Should InstanceType have an isInstance method? + @SuppressWarnings("unchecked") var handle = (InstanceHandleImpl) instanceHandle; + + // Not allowed to steal deleted instances. + if (handle.state instanceof InstanceHandleImpl.Deleted) { + return; + } + // No need to steal if the instance will recreate to us. + if (handle.state instanceof InstanceHandleImpl.Hidden hidden && recreate.equals(hidden.recreate())) { + return; + } + + // FIXME: in theory there could be a race condition here if the instance + // is somehow being stolen by 2 different instancers between threads. + // That seems kinda impossible so I'm fine leaving it as is for now. + + // Add the instance to this instancer. + if (handle.state instanceof InstancePage other) { + if (other.parent == this) { + return; + } + + // Remove the instance from its old instancer. + // This won't have any unwanted effect when the old instancer + // is filtering deleted instances later, so is safe. + other.setDeleted(handle.index); + + // Only lock now that we'll be mutating our state. + addInner(instance, handle); + } else if (handle.state instanceof InstanceHandleImpl.Hidden) { + handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance); + } + } + + private void addInner(I instance, InstanceHandleImpl handle) { + // Outer loop: + // - try to find an empty space + // - or grow the page array if we can't + // - add the instance to the new page, or try again + while (true) { + var pages = this.pages.get(); + + // First, try to find a page with space. + for (int i = fullPages.nextClearBit(0); i < pages.length; i = fullPages.nextClearBit(i + 1)) { + // It may have been filled in while we were searching, but hopefully not. + if (pages[i].add(instance, handle)) { + return; + } + } + + // If we're here, all other pages are full + // If we hit this on the second iteration of the outer loop then `pages` is once again full. + var desiredLength = pages.length + 1; + + // Inner loop: grow the page array. This is very similar to the logic in AtomicBitSet. + while (pages.length < desiredLength) { + // Thread safety: segments contains all pages from the currently visible pages, plus extra. + // all pages in the currently visible pages are canonical and will not change. + // Can't just `new InstancePage[]` because it has a generic parameter. + InstancePage[] newPages = pageArray(desiredLength); + + System.arraycopy(pages, 0, newPages, 0, pages.length); + newPages[pages.length] = new InstancePage<>(this, pages.length); + + // because we are using a compareAndSet, if this thread "wins the race" and successfully sets this variable, then the new page becomes canonical. + if (this.pages.compareAndSet(pages, newPages)) { + pages = newPages; + } else { + // If we "lose the race" and are growing the AtomicBitset segments larger, + // then we will gather the new canonical pages from the update which we missed on the next iteration of this loop. + // The new page will be discarded and never seen again. + pages = this.pages.get(); + } + } + + // Shortcut: try to add the instance to the last page. + // Technically we could just let the outer loop go again, but that + // involves a good bit of work just to likely get back here. + if (pages[pages.length - 1].add(instance, handle)) { + return; + } + // It may be the case that many other instances were added in the same instant. + // We can still lose this race, though it is very unlikely. + } + } + + public int instanceCount() { + // Not exactly accurate but it's an upper bound. + // TODO: maybe this could be tracked with an AtomicInteger? + return pages.get().length << ObjectStorage.LOG_2_PAGE_SIZE; + } + + /** + * Clear all instances without freeing resources. + */ + public void clear() { + this.pages.set(pageArray(0)); + contentsChanged.clear(); + validityChanged.clear(); + fullPages.clear(); + mergeablePages.clear(); + } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java index 9db63afb0..7f78c1e46 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java @@ -40,4 +40,9 @@ public void bind() { GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_LUT, lut.handle(), 0, lut.byteCapacity()); GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_SECTION, sections.handle(), 0, sections.byteCapacity()); } + + public void delete() { + lut.delete(); + sections.delete(); + } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/MatrixBuffer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/MatrixBuffer.java index ce579c465..7328bcb3b 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/MatrixBuffer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/MatrixBuffer.java @@ -30,4 +30,8 @@ public void bind() { GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.MATRICES, matrices.handle(), 0, matrices.byteCapacity()); } + + public void delete() { + matrices.delete(); + } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/ObjectStorage.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/ObjectStorage.java index 56d39613e..d7c7e67b3 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/ObjectStorage.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/ObjectStorage.java @@ -1,6 +1,7 @@ package dev.engine_room.flywheel.backend.engine.indirect; import java.util.Arrays; +import java.util.BitSet; import org.lwjgl.system.MemoryUtil; @@ -13,8 +14,12 @@ public class ObjectStorage extends AbstractArena { public static final int PAGE_SIZE = 1 << LOG_2_PAGE_SIZE; public static final int PAGE_MASK = PAGE_SIZE - 1; + public static final int INVALID_PAGE = -1; + public static final int INITIAL_PAGES_ALLOCATED = 4; + public static final int DESCRIPTOR_SIZE_BYTES = Integer.BYTES * 2; + private final BitSet changedFrames = new BitSet(); /** * The GPU side buffer containing all the objects, logically divided into page frames. */ @@ -28,8 +33,6 @@ public class ObjectStorage extends AbstractArena { */ private MemoryBlock frameDescriptors; - private boolean needsUpload = false; - public ObjectStorage(long objectSizeBytes) { super(PAGE_SIZE * objectSizeBytes); @@ -37,8 +40,8 @@ public ObjectStorage(long objectSizeBytes) { this.frameDescriptorBuffer = new ResizableStorageBuffer(); objectBuffer.ensureCapacity(INITIAL_PAGES_ALLOCATED * elementSizeBytes); - frameDescriptorBuffer.ensureCapacity(INITIAL_PAGES_ALLOCATED * Integer.BYTES); - frameDescriptors = MemoryBlock.malloc(INITIAL_PAGES_ALLOCATED * Integer.BYTES); + frameDescriptorBuffer.ensureCapacity(INITIAL_PAGES_ALLOCATED * DESCRIPTOR_SIZE_BYTES); + frameDescriptors = MemoryBlock.malloc(INITIAL_PAGES_ALLOCATED * DESCRIPTOR_SIZE_BYTES); } public Mapping createMapping() { @@ -52,8 +55,23 @@ public long byteCapacity() { @Override public void free(int i) { + if (i == INVALID_PAGE) { + return; + } super.free(i); - MemoryUtil.memPutInt(ptrForPage(i), 0); + var ptr = ptrForPage(i); + MemoryUtil.memPutInt(ptr, 0); + MemoryUtil.memPutInt(ptr + 4, 0); + + changedFrames.set(i); + } + + private void set(int i, int modelIndex, int validBits) { + var ptr = ptrForPage(i); + MemoryUtil.memPutInt(ptr, modelIndex); + MemoryUtil.memPutInt(ptr + 4, validBits); + + changedFrames.set(i); } @Override @@ -64,12 +82,17 @@ protected void grow() { } public void uploadDescriptors(StagingBuffer stagingBuffer) { - if (!needsUpload) { + if (changedFrames.isEmpty()) { return; } - // We could be smarter about which spans are uploaded but this thing is so small it's probably not worth it. - stagingBuffer.enqueueCopy(frameDescriptors.ptr(), frameDescriptors.size(), frameDescriptorBuffer.handle(), 0); - needsUpload = false; + + var ptr = frameDescriptors.ptr(); + for (int i = changedFrames.nextSetBit(0); i >= 0 && i < capacity(); i = changedFrames.nextSetBit(i + 1)) { + var offset = (long) i * DESCRIPTOR_SIZE_BYTES; + stagingBuffer.enqueueCopy(ptr + offset, DESCRIPTOR_SIZE_BYTES, frameDescriptorBuffer.handle(), offset); + } + + changedFrames.clear(); } public void delete() { @@ -79,7 +102,7 @@ public void delete() { } private long ptrForPage(int page) { - return frameDescriptors.ptr() + (long) page * Integer.BYTES; + return frameDescriptors.ptr() + (long) page * DESCRIPTOR_SIZE_BYTES; } public static int objectIndex2PageIndex(int objectIndex) { @@ -97,61 +120,52 @@ public class Mapping { private static final int[] EMPTY_ALLOCATION = new int[0]; private int[] pages = EMPTY_ALLOCATION; - private int modelIndex = -1; - private int objectCount = 0; + public void updatePage(int index, int modelIndex, int validBits) { + if (validBits == 0) { + holePunch(index); + return; + } + var frame = pages[index]; + + if (frame == INVALID_PAGE) { + // Un-holed punch. + frame = unHolePunch(index); + } + + ObjectStorage.this.set(frame, modelIndex, validBits); + } /** - * Adjust this allocation to the given model index and object count. - * - *

This method triggers eager resizing of the allocation to fit the new object count. - * If the model index is different from the current one, all frame descriptors will be updated. + * Free a page on the inside of the mapping, maintaining the same virtual mapping size. * - * @param modelIndex The model index the objects in this allocation are associated with. - * @param objectCount The number of objects in this allocation. + * @param index The index of the page to free. */ - public void update(int modelIndex, int objectCount) { - boolean incremental = this.modelIndex == modelIndex; - - if (incremental && objectCount == this.objectCount) { - // Nothing will change. - return; - } - - ObjectStorage.this.needsUpload = true; + public void holePunch(int index) { + ObjectStorage.this.free(pages[index]); + pages[index] = INVALID_PAGE; + } - this.modelIndex = modelIndex; - this.objectCount = objectCount; + /** + * Allocate a new page on the inside of the mapping, maintaining the same virtual mapping size. + * + * @param index The index of the page to allocate. + * @return The allocated page. + */ + private int unHolePunch(int index) { + int page = ObjectStorage.this.alloc(); + pages[index] = page; + return page; + } + public void updateCount(int newLength) { var oldLength = pages.length; - var newLength = objectIndex2PageIndex((objectCount + PAGE_MASK)); - if (oldLength > newLength) { // Eagerly free the now unnecessary pages. // shrink will zero out the pageTable entries for the freed pages. shrink(oldLength, newLength); - - if (incremental) { - // Only update the last page, everything else is unchanged. - updateRange(newLength - 1, newLength); - } } else if (oldLength < newLength) { // Allocate new pages to fit the new object count. grow(newLength, oldLength); - - if (incremental) { - // Update the old last page + all new pages - updateRange(oldLength - 1, newLength); - } - } else { - if (incremental) { - // Only update the last page. - updateRange(oldLength - 1, oldLength); - } - } - - if (!incremental) { - // Update all pages. - updateRange(0, newLength); } } @@ -159,8 +173,8 @@ public int pageCount() { return pages.length; } - public long page2ByteOffset(int page) { - return ObjectStorage.this.byteOffsetOf(pages[page]); + public long page2ByteOffset(int index) { + return ObjectStorage.this.byteOffsetOf(pages[index]); } public void delete() { @@ -168,35 +182,6 @@ public void delete() { ObjectStorage.this.free(page); } pages = EMPTY_ALLOCATION; - modelIndex = -1; - objectCount = 0; - - ObjectStorage.this.needsUpload = true; - } - - /** - * Calculates the page descriptor for the given page index. - * Runs under the assumption than all pages are full except maybe the last one. - */ - private int calculatePageDescriptor(int pageIndex) { - int countInPage; - if (objectCount % PAGE_SIZE != 0 && pageIndex == pages.length - 1) { - // Last page && it isn't full -> use the remainder. - countInPage = objectCount & PAGE_MASK; - } else if (objectCount > 0) { - // Full page. - countInPage = PAGE_SIZE; - } else { - // Empty page, this shouldn't be reachable because we eagerly free empty pages. - countInPage = 0; - } - return (modelIndex & 0x3FFFFF) | (countInPage << 26); - } - - private void updateRange(int start, int oldLength) { - for (int i = start; i < oldLength; i++) { - MemoryUtil.memPutInt(ptrForPage(pages[i]), calculatePageDescriptor(i)); - } } private void grow(int neededPages, int oldLength) { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java index 3d2cf0c76..88526f360 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java @@ -147,7 +147,15 @@ protected void initialize(InstancerKey key, InstancedIns @Override public void renderCrumbling(List crumblingBlocks) { // Sort draw calls into buckets, so we don't have to do as many shader binds. - var byType = doCrumblingSort(InstancedInstancer.class, crumblingBlocks); + var byType = doCrumblingSort(crumblingBlocks, handle -> { + // AbstractInstancer directly implement HandleState, so this check is valid. + if (handle instanceof InstancedInstancer instancer) { + return instancer; + } + // This rejects instances that were created by a different engine, + // and also instances that are hidden or deleted. + return null; + }); if (byType.isEmpty()) { return; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java index 2895195a5..c56b281eb 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java @@ -7,7 +7,7 @@ import dev.engine_room.flywheel.api.instance.Instance; import dev.engine_room.flywheel.api.instance.InstanceWriter; -import dev.engine_room.flywheel.backend.engine.AbstractInstancer; +import dev.engine_room.flywheel.backend.engine.BaseInstancer; import dev.engine_room.flywheel.backend.engine.InstancerKey; import dev.engine_room.flywheel.backend.gl.TextureBuffer; import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer; @@ -15,7 +15,7 @@ import dev.engine_room.flywheel.lib.math.MoreMath; import dev.engine_room.flywheel.lib.memory.MemoryBlock; -public class InstancedInstancer extends AbstractInstancer { +public class InstancedInstancer extends BaseInstancer { private final int instanceStride; private final InstanceWriter writer; @@ -109,6 +109,60 @@ public boolean needsToGrow(long capacity) { return capacity > vbo.size(); } + public void parallelUpdate() { + if (deleted.isEmpty()) { + return; + } + + // Figure out which elements are to be removed. + final int oldSize = this.instances.size(); + int removeCount = deleted.cardinality(); + + if (oldSize == removeCount) { + clear(); + return; + } + + final int newSize = oldSize - removeCount; + + // Start from the first deleted index. + int writePos = deleted.nextSetBit(0); + + if (writePos < newSize) { + // Since we'll be shifting everything into this space we can consider it all changed. + changed.set(writePos, newSize); + } + + // We definitely shouldn't consider the deleted instances as changed though, + // else we might try some out of bounds accesses later. + changed.clear(newSize, oldSize); + + // Punch out the deleted instances, shifting over surviving instances to fill their place. + for (int scanPos = writePos; (scanPos < oldSize) && (writePos < newSize); scanPos++, writePos++) { + // Find next non-deleted element. + scanPos = deleted.nextClearBit(scanPos); + + if (scanPos != writePos) { + // Grab the old instance/handle from scanPos... + var handle = handles.get(scanPos); + I instance = instances.get(scanPos); + + // ... and move it to writePos. + handles.set(writePos, handle); + instances.set(writePos, instance); + + // Make sure the handle knows it's been moved + handle.index = writePos; + } + } + + deleted.clear(); + instances.subList(newSize, oldSize) + .clear(); + handles.subList(newSize, oldSize) + .clear(); + } + public void delete() { if (vbo == null) { return; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/RenderSystemMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/RenderSystemMixin.java index 9a51df11b..b08444dc7 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/RenderSystemMixin.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/RenderSystemMixin.java @@ -27,7 +27,10 @@ abstract class RenderSystemMixin { FogUniforms.update(); } - @Inject(method = "setShaderFogShape(Lcom/mojang/blaze3d/shaders/FogShape;)V", at = @At("RETURN")) + // Fabric fails to resolve the mixin in prod when the full signature is specified. + // I suspect it's because this method references a class name in its signature, + // and that needs to be remapped while the function names in RenderSystem are marked with @DontObfuscate. + @Inject(method = "setShaderFogShape", at = @At("RETURN")) private static void flywheel$onSetFogShape(CallbackInfo ci) { FogUniforms.update(); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java b/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java index 334b02732..beeeb403d 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java @@ -43,6 +43,14 @@ public AtomicBitSet(int log2SegmentSizeInBits, int numBitsToPreallocate) { segments = new AtomicReference<>(new AtomicBitSetSegments(numSegmentsToPreallocate, numLongsPerSegment)); } + public void set(int position, boolean value) { + if (value) { + set(position); + } else { + clear(position); + } + } + public void set(int position) { int longPosition = longIndexInSegmentForPosition(position); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl index 395979d5c..96ed0cd81 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl @@ -102,19 +102,14 @@ bool _flw_isVisible(uint instanceIndex, uint modelIndex) { // Clamp to the texture bounds. // Since we're not going through a sampler out of bounds texel fetches will return 0. - bounds = clamp(bounds, ivec4(0), levelSizePair); + bounds = clamp(bounds, ivec4(0), levelSizePair - ivec4(1)); float depth01 = texelFetch(_flw_depthPyramid, bounds.xw, level).r; float depth11 = texelFetch(_flw_depthPyramid, bounds.zw, level).r; float depth10 = texelFetch(_flw_depthPyramid, bounds.zy, level).r; float depth00 = texelFetch(_flw_depthPyramid, bounds.xy, level).r; - float depth; - if (_flw_cullData.useMin == 0) { - depth = max(max(depth00, depth01), max(depth10, depth11)); - } else { - depth = min(min(depth00, depth01), min(depth10, depth11)); - } + float depth = max(max(depth00, depth01), max(depth10, depth11)); float depthSphere = 1. + _flw_cullData.znear / (center.z + radius); @@ -126,24 +121,22 @@ bool _flw_isVisible(uint instanceIndex, uint modelIndex) { } void main() { - uint pageIndex = gl_WorkGroupID.x; + uint pageIndex = gl_WorkGroupID.x << 1u; if (pageIndex >= _flw_pageFrameDescriptors.length()) { return; } - uint packedModelIndexAndCount = _flw_pageFrameDescriptors[pageIndex]; + uint modelIndex = _flw_pageFrameDescriptors[pageIndex]; - uint pageInstanceCount = packedModelIndexAndCount >> _FLW_PAGE_COUNT_OFFSET; + uint pageValidity = _flw_pageFrameDescriptors[pageIndex + 1]; - if (gl_LocalInvocationID.x >= pageInstanceCount) { + if (((1u << gl_LocalInvocationID.x) & pageValidity) == 0) { return; } uint instanceIndex = gl_GlobalInvocationID.x; - uint modelIndex = packedModelIndexAndCount & _FLW_MODEL_INDEX_MASK; - if (_flw_isVisible(instanceIndex, modelIndex)) { uint localIndex = atomicAdd(_flw_models[modelIndex].instanceCount, 1); uint targetIndex = _flw_models[modelIndex].baseInstance + localIndex; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl index 355d2290f..e9ec12039 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl @@ -14,6 +14,8 @@ const float _FLW_EPSILON = 1e-5; const uint _FLW_LOWER_10_BITS = 0x3FFu; const uint _FLW_UPPER_10_BITS = 0xFFF00000u; +const float _FLW_LIGHT_NORMALIZER = 1. / 16.; + uint _flw_indexLut(uint index); uint _flw_indexLight(uint index); @@ -43,18 +45,18 @@ bool _flw_nextLut(uint base, int coord, out uint next) { } bool _flw_chunkCoordToSectionIndex(ivec3 sectionPos, out uint index) { - uint y; - if (_flw_nextLut(0, sectionPos.x, y) || y == 0) { + uint first; + if (_flw_nextLut(0, sectionPos.y, first) || first == 0) { return true; } - uint z; - if (_flw_nextLut(y, sectionPos.y, z) || z == 0) { + uint second; + if (_flw_nextLut(first, sectionPos.x, second) || second == 0) { return true; } uint sectionIndex; - if (_flw_nextLut(z, sectionPos.z, sectionIndex) || sectionIndex == 0) { + if (_flw_nextLut(second, sectionPos.z, sectionIndex) || sectionIndex == 0) { return true; } @@ -98,7 +100,7 @@ bool flw_lightFetch(ivec3 blockPos, out vec2 lightCoord) { uvec3 blockInSectionPos = (blockPos & 0xF) + 1; - lightCoord = vec2(_flw_lightAt(sectionOffset, blockInSectionPos)) / 15.; + lightCoord = vec2(_flw_lightAt(sectionOffset, blockInSectionPos)) * _FLW_LIGHT_NORMALIZER; return true; } @@ -298,7 +300,7 @@ vec3 _flw_lightForDirection(uint[27] lights, vec3 interpolant, uint c00, uint c0 vec3 light = mix(light0, light1, interpolant.y); // Normalize the light coords - light.xy *= 1. / 15.; + light.xy *= _FLW_LIGHT_NORMALIZER; // Calculate the AO multiplier from the number of valid blocks light.z = _flw_validCountToAo(light.z); @@ -351,7 +353,7 @@ bool flw_light(vec3 worldPos, vec3 normal, out FlwLightAo light) { vec2 light0 = mix(light00, light01, interpolant.y); vec2 light1 = mix(light10, light11, interpolant.y); - light.light = mix(light0, light1, interpolant.x) / 15.; + light.light = mix(light0, light1, interpolant.x) * _FLW_LIGHT_NORMALIZER; light.ao = 1.; // Lighting and AO accurate to chunk baking @@ -410,7 +412,7 @@ bool flw_light(vec3 worldPos, vec3 normal, out FlwLightAo light) { // Entirely flat lighting, the lowest setting and a fallback in case an invalid option is set #else - light.light = vec2(_flw_lightAt(sectionOffset, blockInSectionPos)) / 15.; + light.light = vec2(_flw_lightAt(sectionOffset, blockInSectionPos)) * _FLW_LIGHT_NORMALIZER; light.ao = 1.; #endif diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java index b78562b0b..40522c89d 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java @@ -11,7 +11,6 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; -// TODO: Add freezing @SuppressWarnings("unchecked") public final class VisualizerRegistryImpl { @Nullable diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java index 052a9e06c..804f98bae 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java @@ -43,7 +43,11 @@ public void add(ShaderLightVisual visual, SectionTracker tracker) { } public void remove(ShaderLightVisual visual) { - trackers.remove(visual); + var tracker = trackers.remove(visual); + + if (tracker != null) { + markDirty(); + } } public void clear() { diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java index 53efc4562..3516cd041 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java @@ -7,26 +7,6 @@ import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.block.entity.BlockEntityType; -/** - * TODO: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
{@link BlockEntityType#SIGN} {@link net.minecraft.client.renderer.blockentity.SignRenderer SignRenderer}
{@link BlockEntityType#PISTON} {@link net.minecraft.client.renderer.blockentity.PistonHeadRenderer PistonHeadRenderer}
{@link BlockEntityType#CONDUIT} {@link net.minecraft.client.renderer.blockentity.ConduitRenderer ConduitRenderer}
{@link BlockEntityType#ENCHANTING_TABLE} {@link net.minecraft.client.renderer.blockentity.EnchantTableRenderer EnchantTableRenderer}
{@link BlockEntityType#LECTERN} {@link net.minecraft.client.renderer.blockentity.LecternRenderer LecternRenderer}
{@link BlockEntityType#MOB_SPAWNER} {@link net.minecraft.client.renderer.blockentity.SpawnerRenderer SpawnerRenderer}
{@link BlockEntityType#BED} {@link net.minecraft.client.renderer.blockentity.BedRenderer BedRenderer}
^^ Interesting - Major vv
{@link BlockEntityType#END_PORTAL} {@link net.minecraft.client.renderer.blockentity.TheEndPortalRenderer TheEndPortalRenderer}
{@link BlockEntityType#END_GATEWAY} {@link net.minecraft.client.renderer.blockentity.TheEndGatewayRenderer TheEndGatewayRenderer}
{@link BlockEntityType#BEACON} {@link net.minecraft.client.renderer.blockentity.BeaconRenderer BeaconRenderer}
{@link BlockEntityType#SKULL} {@link net.minecraft.client.renderer.blockentity.SkullBlockRenderer SkullBlockRenderer}
{@link BlockEntityType#BANNER} {@link net.minecraft.client.renderer.blockentity.BannerRenderer BannerRenderer}
{@link BlockEntityType#STRUCTURE_BLOCK} {@link net.minecraft.client.renderer.debug.StructureRenderer StructureRenderer}
{@link BlockEntityType#CAMPFIRE} {@link net.minecraft.client.renderer.blockentity.CampfireRenderer CampfireRenderer}
- */ public class VanillaVisuals { public static void init() { builder(BlockEntityType.CHEST) diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 9a606f33b..acba756a2 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -11,6 +11,7 @@ val api = sourceSets.create("api") val lib = sourceSets.create("lib") val backend = sourceSets.create("backend") val main = sourceSets.getByName("main") +val testMod = sourceSets.create("testMod") transitiveSourceSets { compileClasspath = main.compileClasspath @@ -29,6 +30,9 @@ transitiveSourceSets { sourceSet(main) { implementation(api, lib, backend) } + sourceSet(testMod) { + rootCompile() + } createCompileConfigurations() } @@ -39,6 +43,7 @@ platform { setupLoomMod(api, lib, backend, main) setupLoomRuns() setupFatJar(api, lib, backend, main) + setupTestMod(testMod) } jarSets { diff --git a/fabric/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java b/fabric/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java new file mode 100644 index 000000000..6b05b1e62 --- /dev/null +++ b/fabric/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java @@ -0,0 +1,29 @@ +package dev.engine_room.flywheel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.MixinEnvironment; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; + +public class FlywheelTestModClient implements ClientModInitializer { + public static final String NAME = "Flywheel Test Mod"; + private static final Logger LOGGER = LoggerFactory.getLogger(NAME); + + @Override + public void onInitializeClient() { + LOGGER.info("Starting {} on EnvType: {}", NAME, FabricLoader.getInstance() + .getEnvironmentType()); + + ClientLifecycleEvents.CLIENT_STARTED.register(client -> { + LOGGER.info("Running mixin audit"); + MixinEnvironment.getCurrentEnvironment() + .audit(); + + LOGGER.info("Stopping client"); + client.stop(); + }); + } +} diff --git a/fabric/src/testMod/resources/fabric.mod.json b/fabric/src/testMod/resources/fabric.mod.json new file mode 100644 index 000000000..11bd545a1 --- /dev/null +++ b/fabric/src/testMod/resources/fabric.mod.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "id" : "${mod_id}_testmod", + "name": "${mod_name} Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "${mod_license}", + "entrypoints": { + "client": [ + "dev.engine_room.flywheel.FlywheelTestModClient" + ] + } +} diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 68e2442ba..b1383ff3f 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -11,6 +11,7 @@ val api = sourceSets.create("api") val lib = sourceSets.create("lib") val backend = sourceSets.create("backend") val main = sourceSets.getByName("main") +val testMod = sourceSets.create("testMod") transitiveSourceSets { compileClasspath = main.compileClasspath @@ -29,6 +30,9 @@ transitiveSourceSets { sourceSet(main) { compile(api, lib, backend) } + sourceSet(testMod) { + rootCompile() + } createCompileConfigurations() } @@ -39,6 +43,7 @@ platform { setupLoomMod(api, lib, backend, main) setupLoomRuns() setupFatJar(api, lib, backend, main) + setupTestMod(testMod) } jarSets { diff --git a/forge/src/main/resources/META-INF/neoforge.mods.toml b/forge/src/main/resources/META-INF/neoforge.mods.toml index 1954a3f6d..f5d547387 100644 --- a/forge/src/main/resources/META-INF/neoforge.mods.toml +++ b/forge/src/main/resources/META-INF/neoforge.mods.toml @@ -37,7 +37,6 @@ mandatory = false versionRange = "[0.6.0-beta.2,)" side = "CLIENT" -# Simulates a breaks/incompatible dependency [[dependencies.${mod_id}]] modId = "embeddium" type = "incompatible" diff --git a/forge/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java b/forge/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java new file mode 100644 index 000000000..f04eb604f --- /dev/null +++ b/forge/src/testMod/java/dev/engine_room/flywheel/FlywheelTestModClient.java @@ -0,0 +1,33 @@ +package dev.engine_room.flywheel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.MixinEnvironment; + +import net.minecraft.client.Minecraft; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.loading.FMLLoader; + +@Mod("flywheel_testmod") +public class FlywheelTestModClient { + public static final String NAME = "Flywheel Test Mod"; + private static final Logger LOGGER = LoggerFactory.getLogger(NAME); + + public FlywheelTestModClient() { + LOGGER.info("Starting {} on Dist: {}", NAME, FMLLoader.getDist()); + + MinecraftForge.EVENT_BUS.addListener((TickEvent.ClientTickEvent e) -> { + if (e.phase == TickEvent.Phase.END) { + LOGGER.info("Running mixin audit"); + MixinEnvironment.getCurrentEnvironment() + .audit(); + + LOGGER.info("Stopping client"); + Minecraft.getInstance() + .stop(); + } + }); + } +} diff --git a/forge/src/testMod/resources/META-INF/mods.toml b/forge/src/testMod/resources/META-INF/mods.toml new file mode 100644 index 000000000..4d76a503e --- /dev/null +++ b/forge/src/testMod/resources/META-INF/mods.toml @@ -0,0 +1,8 @@ +modLoader = "javafml" +loaderVersion = "[0,)" +license = "${mod_license}" + +[[mods]] +modId = "${mod_id}_testmod" +version = "1.0.0" +displayName = "${mod_name} Test Mod"