Compare commits

...

16 Commits

Author SHA1 Message Date
Jozufozu
a6c5f93fb4 Spherical instances in a vacuum
- Write out instance bounding spheres in pass one
- Read them back in pass two so we don't have to read in the entire
  instance twice
- Cull pass 2 no longer needs to be parameterized by instance type, so
  less program binds are needed
- Fix page indexing logic
- Fix visibility sizing logic
2024-11-03 16:32:29 -08:00
Jozufozu
1823a9fa24 Merge branch '1.20/dev' into 1.20/last-frame-visibility
# Conflicts:
#	common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/late_cull.glsl
2024-11-03 12:52:35 -08:00
Jozufozu
4f0c1cc1ae Checking the source
- Remove source checks, it was very old and untouched
- Remove FIXMEs and TODOs that were already fixed/done
2024-11-03 12:15:04 -08:00
Jozufozu
540fe7a7fe Acetaminophen
- Generic pain relief
- Use new Instance[] rather than capturing the class object of the
  instance type
- Make InstancePage static, but manually track the instancer parent so
  we can check when stealing
- Simplify array creation helpers and make them static
- Mark InstanceHandleImpl#state as UnknownNullability
2024-11-02 16:56:49 -07:00
Jozufozu
a9f2018c0a The epitome of laziness
- Only upload changed page frame descriptors
- In the instancer, track changed contents separately from changed
  validity, so we can separately upload objects and descriptors
2024-11-02 14:12:45 -07:00
Jozufozu
fc3e475ec9 Gattai!
- Combine pages only when they're at most half full, and not empty
- This guarantees that we'll fully empty a page, allowing us to free the memory for use by other instancers
- Track mergeable pages via a separate bitset
2024-11-01 23:50:06 -07:00
Jozufozu
fac63168c1 Bookkeeping
- Mappings drop pages when they write zero validity bits
- Instancer only updates pages that changed
2024-11-01 12:47:15 -07:00
Jozufozu
20b3f78b9c A real page turner
- Try to shuffle over instances into pages with space
- Clear out now-unused logic from ObjectStorage
- Some cleanup and more comments in IndirectInstancer
2024-10-29 20:02:38 -07:00
Jozufozu
a7e7090866 Finding an old bookmark
- Make AbstractInstancer much more slim and move logic to BaseInstancer
- Extend paging concept to the indirect instancer
- Extend ObjectStorage to support more interesting layouts
- Instance creation on indirect is now entirely lock free and deletions
  no longer require re-uploading the entire instancer
2024-10-29 20:02:37 -07:00
Jozufozu
5862f3746c A clampy merge
- Remove dead code leftover from merge
- Add back clamping in late cull
2024-10-19 17:14:10 -07:00
Jozufozu
be7676eeb4 Merge branch '1.20/dev' into 1.20/last-frame-visibility 2024-10-19 16:59:09 -07:00
Jozufozu
3a949c717a Turn the cutout off crash off
- MaterialEncoder would trigger an indexing of CutoutShaders.OFF, though
  PipelineCompiler would explicitly not index OFF
- This caused a crash on instancing when MaterialEncoder would delete
  all pipeline shaders while instancing was trying to upload the packed
  ubershader uniform
2024-10-19 16:58:04 -07:00
IThundxr
3d2fdb7e83
Backport changes from 1.21.1 (#265)
* Backport changes from 1.21.1

* fix

* Fix building

* fix compile error

* fix

* fix build for real

* address reviews

* Fix sodium compat

* address requested changes

* mark rubidium as incompatible

* add missed call

* Should have worn steel toe boots

- Add "stub" sourceset to each subproject
- Directly pass vararg sourcesets to methods in PlatformExtension to
  avoid automatically shipping jars with the api stubs
- We may have to include stubs in setupLoomMod, but I don't think so
- A lot of this can be stripped back out if we don't need stub sources
  for the forge/fabric subprojects

* Guarded stubs

- Add Sodium 0.6 and Iris API stubs to stubs source set and remove Gradle dependencies on local Sodium jar, Iris, and Oculus
- Ensure usage of APIs that may not exist at runtime is in private classes and access is always guarded
- Change ShadersModHandler
  - Rename to ShadersModHelper
  - Convert methods to check for Iris' and Optifine's presence into static final fields
  - Move implementation to impl source set in form of IrisCompat and OptifineCompat classes
- Rename CompatMods to CompatMod and add public field to access mod ID
- Set BlockEntityType's Sodium predicate to null after it is removed
- Update repository links
- Remove local libs repository

---------

Co-authored-by: Jozufozu <jozsefaug@gmail.com>
Co-authored-by: PepperCode1 <44146161+PepperCode1@users.noreply.github.com>
2024-10-18 20:29:43 -07:00
PepperCode1
734506b010 Fix #266 2024-10-18 18:25:11 -07:00
Jozufozu
3193e16498 Weak assumption
- Use a weak hash set for PipelineCompiler.ALL
2024-10-12 17:02:50 -07:00
Jozufozu
efb68dc776 All or nothing?
- Actually add PipelineCompilers into the ALL list so they get deleted
  when uber components need updating
2024-10-12 16:36:20 -07:00
68 changed files with 1506 additions and 890 deletions

View File

@ -23,16 +23,11 @@ open class PlatformExtension(val project: Project) {
var apiArtifactId: String = "flywheel-${project.name}-api-${project.property("artifact_minecraft_version")}"
private val sources = mutableSetOf<SourceSet>()
private val commonSourceSets: SourceSetContainer by lazy { commonProject.the<SourceSetContainer>() }
fun sources(vararg sourceSets: SourceSet) {
this.sources.addAll(sourceSets)
}
fun setupLoomMod() {
fun setupLoomMod(vararg sourceSets: SourceSet) {
project.the<LoomGradleExtensionAPI>().mods.maybeCreate("main").apply {
sources.forEach(::sourceSet)
sourceSets.forEach(::sourceSet)
}
}
@ -61,13 +56,13 @@ open class PlatformExtension(val project: Project) {
}
}
fun compileWithCommonSourceSets() {
fun compileWithCommonSourceSets(vararg sourceSets: SourceSet) {
project.tasks.apply {
withType<JavaCompile>().configureEach {
JarTaskSet.excludeDuplicatePackageInfos(this)
}
sources.forEach {
sourceSets.forEach {
val commonSourceSet = commonSourceSets.named(it.name).get()
named<JavaCompile>(it.compileJavaTaskName).configure {
@ -80,10 +75,10 @@ open class PlatformExtension(val project: Project) {
}
}
fun setupFatJar() {
fun setupFatJar(vararg sourceSets: SourceSet) {
project.tasks.apply {
val extraSourceSets = sources.filter { it.name != "main" }.toList()
val commonSources = sources.map { commonSourceSets.named(it.name).get() }
val extraSourceSets = sourceSets.filter { it.name != "main" }.toList()
val commonSources = sourceSets.map { commonSourceSets.named(it.name).get() }
named<Jar>("jar").configure {
extraSourceSets.forEach { from(it.output) }

View File

@ -96,6 +96,7 @@ class SubprojectPlugin: Plugin<Project> {
private fun setupDependencies(project: Project) {
project.dependencies.apply {
val minecraft_version: String by project
val parchment_minecraft_version: String by project
val parchment_version: String by project
val loom = project.the<LoomGradleExtensionAPI>()
@ -103,7 +104,7 @@ class SubprojectPlugin: Plugin<Project> {
add("mappings", loom.layered {
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-${minecraft_version}:${parchment_version}@zip")
parchment("org.parchmentmc.data:parchment-${parchment_minecraft_version}:${parchment_version}@zip")
})
add("api", "com.google.code.findbugs:jsr305:3.0.2")

View File

@ -25,7 +25,7 @@ open class TransitiveSourceSetsExtension(private val project: Project) {
fun createCompileConfigurations() {
val configs = transitives.mapValues { (sourceSet, _) ->
project.configurations.create("for${sourceSet.name.capitalize()}") {
project.configurations.create("for${sourceSet.name.replaceFirstChar { it.uppercase() }}") {
isCanBeConsumed = true
isCanBeResolved = false
}
@ -43,7 +43,7 @@ open class TransitiveSourceSetsExtension(private val project: Project) {
fun createRuntimeConfigurations() {
val configs = transitives.mapValues { (sourceSet, _) ->
project.configurations.create("run${sourceSet.name.capitalize()}") {
project.configurations.create("run${sourceSet.name.replaceFirstChar { it.uppercase() }}") {
isCanBeConsumed = true
isCanBeResolved = false
}

View File

@ -9,6 +9,7 @@ plugins {
val api = sourceSets.create("api")
val lib = sourceSets.create("lib")
val backend = sourceSets.create("backend")
val stubs = sourceSets.create("stubs")
val main = sourceSets.getByName("main")
transitiveSourceSets {
@ -25,8 +26,11 @@ transitiveSourceSets {
rootCompile()
compile(api, lib)
}
sourceSet(stubs) {
rootCompile()
}
sourceSet(main) {
compile(api, lib, backend)
compile(api, lib, backend, stubs)
}
sourceSet(sourceSets.getByName("test")) {
implementation(api, lib, backend)
@ -42,6 +46,7 @@ jarSets {
outgoing("commonApiOnly", api)
outgoing("commonLib", lib)
outgoing("commonBackend", backend)
outgoing("commonStubs", stubs)
outgoing("commonImpl", main)
// For publishing.

View File

@ -9,7 +9,7 @@ import dev.engine_room.flywheel.backend.engine.indirect.IndirectDrawManager;
import dev.engine_room.flywheel.backend.engine.instancing.InstancedDrawManager;
import dev.engine_room.flywheel.backend.gl.GlCompat;
import dev.engine_room.flywheel.lib.backend.SimpleBackend;
import dev.engine_room.flywheel.lib.util.ShadersModHandler;
import dev.engine_room.flywheel.lib.util.ShadersModHelper;
public final class Backends {
/**
@ -18,7 +18,7 @@ public final class Backends {
public static final Backend INSTANCING = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(level, new InstancedDrawManager(InstancingPrograms.get()), 256))
.priority(500)
.supported(() -> GlCompat.SUPPORTS_INSTANCING && InstancingPrograms.allLoaded() && !ShadersModHandler.isShaderPackInUse())
.supported(() -> GlCompat.SUPPORTS_INSTANCING && InstancingPrograms.allLoaded() && !ShadersModHelper.isShaderPackInUse())
.register(Flywheel.rl("instancing"));
/**
@ -27,7 +27,7 @@ public final class Backends {
public static final Backend INDIRECT = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(level, new IndirectDrawManager(IndirectPrograms.get()), 256))
.priority(1000)
.supported(() -> GlCompat.SUPPORTS_INDIRECT && IndirectPrograms.allLoaded() && !ShadersModHandler.isShaderPackInUse())
.supported(() -> GlCompat.SUPPORTS_INDIRECT && IndirectPrograms.allLoaded() && !ShadersModHelper.isShaderPackInUse())
.register(Flywheel.rl("indirect"));
private Backends() {

View File

@ -23,6 +23,7 @@ import dev.engine_room.flywheel.backend.glsl.SourceComponent;
import dev.engine_room.flywheel.backend.util.AtomicReferenceCounted;
import dev.engine_room.flywheel.lib.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Unit;
public class IndirectPrograms extends AtomicReferenceCounted {
private static final ResourceLocation CULL_SHADER_API_IMPL = Flywheel.rl("internal/indirect/cull_api_impl.glsl");
@ -34,10 +35,10 @@ public class IndirectPrograms extends AtomicReferenceCounted {
private static final ResourceLocation ZERO_MODELS_SHADER_MAIN = Flywheel.rl("internal/indirect/zero_models.glsl");
private static final ResourceLocation DOWNSAMPLE_FIRST = Flywheel.rl("internal/indirect/downsample_first.glsl");
private static final ResourceLocation DOWNSAMPLE_SECOND = Flywheel.rl("internal/indirect/downsample_second.glsl");
public static final List<ResourceLocation> UTIL_SHADERS = List.of(APPLY_SHADER_MAIN, SCATTER_SHADER_MAIN, READ_VISIBILITY_SHADER_MAIN, ZERO_MODELS_SHADER_MAIN, DOWNSAMPLE_FIRST, DOWNSAMPLE_SECOND);
private static final Compile<InstanceType<?>> CULL = new Compile<>();
private static final Compile<ResourceLocation> UTIL = new Compile<>();
private static final Compile<Unit> UNIT = new Compile<>();
private static final List<String> EXTENSIONS = getExtensions(GlCompat.MAX_GLSL_VERSION);
private static final List<String> COMPUTE_EXTENSIONS = getComputeExtensions(GlCompat.MAX_GLSL_VERSION);
@ -47,10 +48,10 @@ public class IndirectPrograms extends AtomicReferenceCounted {
private final PipelineCompiler pipeline;
private final CompilationHarness<InstanceType<?>> culling;
private final CompilationHarness<InstanceType<?>> cullPassTwo;
private final CompilationHarness<Unit> cullPassTwo;
private final CompilationHarness<ResourceLocation> utils;
private IndirectPrograms(PipelineCompiler pipeline, CompilationHarness<InstanceType<?>> culling, CompilationHarness<InstanceType<?>> cullPassTwo, CompilationHarness<ResourceLocation> utils) {
private IndirectPrograms(PipelineCompiler pipeline, CompilationHarness<InstanceType<?>> culling, CompilationHarness<Unit> cullPassTwo, CompilationHarness<ResourceLocation> utils) {
this.pipeline = pipeline;
this.culling = culling;
this.cullPassTwo = cullPassTwo;
@ -92,7 +93,7 @@ public class IndirectPrograms extends AtomicReferenceCounted {
var pipelineCompiler = PipelineCompiler.create(sources, Pipelines.INDIRECT, vertexComponents, fragmentComponents, EXTENSIONS);
var pass1Compiler = createCullingCompiler(sources, CULL_SHADER_MAIN, "early_cull");
var pass2Compiler = createCullingCompiler(sources, PASS2_SHADER_MAIN, "late_cull");
var pass2Compiler = createPassTwoCompiler(sources, PASS2_SHADER_MAIN, "late_cull");
var utilCompiler = createUtilCompiler(sources);
IndirectPrograms newInstance = new IndirectPrograms(pipelineCompiler, pass1Compiler, pass2Compiler, utilCompiler);
@ -120,6 +121,19 @@ public class IndirectPrograms extends AtomicReferenceCounted {
.harness(name, sources);
}
private static CompilationHarness<Unit> createPassTwoCompiler(ShaderSources sources, ResourceLocation main, String name) {
return UNIT.program()
.link(UNIT.shader(GlCompat.MAX_GLSL_VERSION, ShaderType.COMPUTE)
.nameMapper(instanceType -> name)
.requireExtensions(COMPUTE_EXTENSIONS)
.define("_FLW_SUBGROUP_SIZE", GlCompat.SUBGROUP_SIZE)
.enableExtension("GL_KHR_shader_subgroup_basic")
.enableExtension("GL_KHR_shader_subgroup_ballot")
.withResource(main))
.postLink((key, program) -> Uniforms.setUniformBlockBindings(program))
.harness(name, sources);
}
/**
* A compiler for utility shaders, directly compiles the shader at the resource location specified by the parameter.
*/
@ -164,8 +178,8 @@ public class IndirectPrograms extends AtomicReferenceCounted {
return culling.get(instanceType);
}
public GlProgram getCullPassTwoProgram(InstanceType<?> instanceType) {
return cullPassTwo.get(instanceType);
public GlProgram getCullPassTwoProgram() {
return cullPassTwo.get(Unit.INSTANCE);
}
public GlProgram getApplyProgram() {

View File

@ -1,7 +1,10 @@
package dev.engine_room.flywheel.backend.compile;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.instance.InstanceType;
@ -30,7 +33,7 @@ import dev.engine_room.flywheel.lib.util.ResourceUtil;
import net.minecraft.resources.ResourceLocation;
public final class PipelineCompiler {
private static final List<PipelineCompiler> ALL = List.of();
private static final Set<PipelineCompiler> ALL = Collections.newSetFromMap(new WeakHashMap<>());
private static final Compile<PipelineProgramKey> PIPELINE = new Compile<>();
@ -44,6 +47,7 @@ public final class PipelineCompiler {
public PipelineCompiler(CompilationHarness<PipelineProgramKey> harness) {
this.harness = harness;
ALL.add(this);
}
public GlProgram get(InstanceType<?> instanceType, ContextShader contextShader, Material material) {
@ -57,15 +61,12 @@ public final class PipelineCompiler {
MaterialShaderIndices.fogSources()
.index(fog.source());
boolean useCutout = cutout != CutoutShaders.OFF;
// Same thing for cutout.
// Add OFF to the index here anyway to ensure MaterialEncoder doesn't deleteAll at an inappropriate time.
MaterialShaderIndices.cutoutSources()
.index(cutout.source());
if (useCutout) {
// Same thing for cutout.
MaterialShaderIndices.cutoutSources()
.index(cutout.source());
}
return harness.get(new PipelineProgramKey(instanceType, contextShader, light, shaders, useCutout, FrameUniforms.debugOn()));
return harness.get(new PipelineProgramKey(instanceType, contextShader, light, shaders, cutout != CutoutShaders.OFF, FrameUniforms.debugOn()));
}
public void delete() {

View File

@ -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<ErrorReporter, SourceFile> LAYOUT_VERTEX = checkFunctionArity("flw_layoutVertex", 0);
// public static final BiConsumer<ErrorReporter, SourceFile> INSTANCE_VERTEX = checkFunctionParameterTypeExists("flw_instanceVertex", 1, 0);
// public static final BiConsumer<ErrorReporter, SourceFile> MATERIAL_VERTEX = checkFunctionArity("flw_materialVertex", 0);
// public static final BiConsumer<ErrorReporter, SourceFile> MATERIAL_FRAGMENT = checkFunctionArity("flw_materialFragment", 0);
// public static final BiConsumer<ErrorReporter, SourceFile> CONTEXT_VERTEX = checkFunctionArity("flw_contextVertex", 0);
// public static final BiConsumer<ErrorReporter, SourceFile> CONTEXT_FRAGMENT = checkFunctionArity("flw_contextFragment", 0).andThen(checkFunctionArity("flw_initFragment", 0));
// public static final BiConsumer<ErrorReporter, SourceFile> PIPELINE = checkFunctionArity("main", 0);
//
// public static BiConsumer<ErrorReporter, SourceFile> checkFunctionArity(String name, int arity) {
// return (errorReporter, file) -> checkFunctionArity(errorReporter, file, name, arity);
// }
//
// public static BiConsumer<ErrorReporter, SourceFile> 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<ShaderFunction> maybeFunc = file.findFunction(name);
//
// if (maybeFunc.isEmpty()) {
// errorReporter.generateMissingFunction(file, name, "\"" + name + "\" function not defined");
// return null;
// }
//
// ShaderFunction func = maybeFunc.get();
// ImmutableList<ShaderVariable> params = func.getParameters();
// if (params.size() != arity) {
// errorReporter.generateFunctionArgumentCountError(name, arity, func.getArgs());
// return null;
// }
//
// return func;
// }
}

View File

@ -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;

View File

@ -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<I extends Instance> implements Instancer<I>, InstanceHandleImpl.State<I> {
public abstract class AbstractInstancer<I extends Instance> implements Instancer<I> {
public final InstanceType<I> type;
public final Environment environment;
private final Recreate<I> 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<I> instances = new ArrayList<>();
protected final ArrayList<InstanceHandleImpl<I>> handles = new ArrayList<>();
protected final AtomicBitSet changed = new AtomicBitSet();
protected final AtomicBitSet deleted = new AtomicBitSet();
public final Recreate<I> recreate;
protected AbstractInstancer(InstancerKey<I> key, Recreate<I> recreate) {
this.type = key.type();
@ -29,218 +16,16 @@ public abstract class AbstractInstancer<I extends Instance> implements Instancer
this.recreate = recreate;
}
@Override
public InstanceHandleImpl.State<I> setChanged(int index) {
notifyDirty(index);
return this;
}
public abstract InstanceHandleImpl.State<I> revealInstance(InstanceHandleImpl<I> handle, I instance);
@Override
public InstanceHandleImpl.State<I> setDeleted(int index) {
notifyRemoval(index);
return InstanceHandleImpl.Deleted.instance();
}
public abstract int instanceCount();
@Override
public InstanceHandleImpl.State<I> setVisible(InstanceHandleImpl<I> 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<I> 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<I>) 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<I> 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<I> 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<I>) {
handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance);
}
}
/**
* Calls must be synchronized on {@link #lock}.
*/
private void addLocked(I instance, InstanceHandleImpl<I> 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);
}
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<I> 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() + ']';

View File

@ -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<I extends Instance> extends AbstractInstancer<I> implements InstanceHandleImpl.State<I> {
// 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<I> instances = new ArrayList<>();
protected final ArrayList<InstanceHandleImpl<I>> handles = new ArrayList<>();
protected final AtomicBitSet changed = new AtomicBitSet();
protected final AtomicBitSet deleted = new AtomicBitSet();
protected BaseInstancer(InstancerKey<I> key, Recreate<I> recreate) {
super(key, recreate);
}
@Override
public InstanceHandleImpl.State<I> setChanged(int index) {
notifyDirty(index);
return this;
}
@Override
public InstanceHandleImpl.State<I> setDeleted(int index) {
notifyRemoval(index);
return InstanceHandleImpl.Deleted.instance();
}
@Override
public InstanceHandleImpl.State<I> setVisible(InstanceHandleImpl<I> 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<I> revealInstance(InstanceHandleImpl<I> 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<I>) 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<I> 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<I> 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<I>) {
handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance);
}
}
/**
* Calls must be synchronized on {@link #lock}.
*/
private void addLocked(I instance, InstanceHandleImpl<I> 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<I> 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();
}
}

View File

@ -50,7 +50,7 @@ public abstract class DrawManager<N extends AbstractInstancer<?>> {
public Plan<RenderContext> 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) {

View File

@ -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<I extends Instance> implements InstanceHandle {
@UnknownNullability
public State<I> state;
public int index;
public InstanceHandleImpl(State<I> state) {
public InstanceHandleImpl(@UnknownNullability State<I> state) {
this.state = state;
}
@ -62,8 +65,7 @@ public class InstanceHandleImpl<I extends Instance> implements InstanceHandle {
return this;
}
var instancer = recreate.recreate();
instancer.revealInstance(handle, instance);
return instancer;
return instancer.revealInstance(handle, instance);
}
}

View File

@ -22,7 +22,6 @@ import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.lighting.LayerLightEventListener;
/**
* TODO: AO data
* A managed arena of light sections for uploading to the GPU.
*
* <p>Each section represents an 18x18x18 block volume of light data.
@ -116,7 +115,6 @@ public class LightStorage {
}
// Now actually do the collection.
// TODO: Should this be done in parallel?
sectionsToCollect.forEach(this::collectSection);
updatedSections.clear();

View File

@ -1,16 +1,17 @@
package dev.engine_room.flywheel.backend.engine.indirect;
public final class BufferBindings {
public static final int LAST_FRAME_VISIBILITY = 0;
public static final int PAGE_FRAME_DESCRIPTOR = 1;
public static final int INSTANCE = 2;
public static final int DRAW_INSTANCE_INDEX = 3;
public static final int MODEL = 4;
public static final int DRAW = 5;
public static final int BOUNDING_SPHERES = 0;
public static final int LAST_FRAME_VISIBILITY = 1;
public static final int PAGE_FRAME_DESCRIPTOR = 2;
public static final int INSTANCE = 3;
public static final int DRAW_INSTANCE_INDEX = 4;
public static final int MODEL = 5;
public static final int DRAW = 6;
public static final int LIGHT_LUT = 6;
public static final int LIGHT_SECTION = 7;
public static final int MATRICES = 8;
public static final int LIGHT_LUT = 7;
public static final int LIGHT_SECTION = 8;
public static final int MATRICES = 9;
private BufferBindings() {
}

View File

@ -7,12 +7,11 @@ import org.lwjgl.system.MemoryUtil;
import org.lwjgl.system.Pointer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBufferType;
import dev.engine_room.flywheel.lib.math.MoreMath;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
public class IndirectBuffers {
// Number of vbos created.
public static final int BUFFER_COUNT = 6;
public static final int BUFFER_COUNT = 7;
public static final long INT_SIZE = Integer.BYTES;
public static final long PTR_SIZE = Pointer.POINTER_SIZE;
@ -31,6 +30,7 @@ public class IndirectBuffers {
private static final long BUFFERS_SIZE_BYTES = SIZE_OFFSET + BUFFER_COUNT * PTR_SIZE;
// Offsets to the vbos
private static final long BOUNDING_SPHERES_HANDLE_OFFSET = HANDLE_OFFSET + BufferBindings.BOUNDING_SPHERES * INT_SIZE;
private static final long LAST_FRAME_VISIBILITY_HANDLE_OFFSET = HANDLE_OFFSET + BufferBindings.LAST_FRAME_VISIBILITY * INT_SIZE;
private static final long PAGE_FRAME_DESCRIPTOR_HANDLE_OFFSET = HANDLE_OFFSET + BufferBindings.PAGE_FRAME_DESCRIPTOR * INT_SIZE;
private static final long INSTANCE_HANDLE_OFFSET = HANDLE_OFFSET + BufferBindings.INSTANCE * INT_SIZE;
@ -39,6 +39,7 @@ public class IndirectBuffers {
private static final long DRAW_HANDLE_OFFSET = HANDLE_OFFSET + BufferBindings.DRAW * INT_SIZE;
// Offsets to the sizes
private static final long BOUNDING_SPHERES_SIZE_OFFSET = SIZE_OFFSET + BufferBindings.BOUNDING_SPHERES * PTR_SIZE;
private static final long LAST_FRAME_VISIBILITY_SIZE_OFFSET = SIZE_OFFSET + BufferBindings.LAST_FRAME_VISIBILITY * PTR_SIZE;
private static final long PAGE_FRAME_DESCRIPTOR_SIZE_OFFSET = SIZE_OFFSET + BufferBindings.PAGE_FRAME_DESCRIPTOR * PTR_SIZE;
private static final long INSTANCE_SIZE_OFFSET = SIZE_OFFSET + BufferBindings.INSTANCE * PTR_SIZE;
@ -65,6 +66,7 @@ public class IndirectBuffers {
*/
private final MemoryBlock multiBindBlock;
public final ResizableStorageArray boundingSpheres;
public final ResizableStorageArray lastFrameVisibility;
public final ObjectStorage objectStorage;
public final ResizableStorageArray drawInstanceIndex;
@ -74,6 +76,7 @@ public class IndirectBuffers {
IndirectBuffers(long instanceStride) {
this.multiBindBlock = MemoryBlock.calloc(BUFFERS_SIZE_BYTES, 1);
boundingSpheres = new ResizableStorageArray(16);
lastFrameVisibility = new ResizableStorageArray(INT_SIZE, INSTANCE_GROWTH_FACTOR);
objectStorage = new ObjectStorage(instanceStride);
drawInstanceIndex = new ResizableStorageArray(INT_SIZE, INSTANCE_GROWTH_FACTOR);
@ -83,12 +86,14 @@ public class IndirectBuffers {
void updateCounts(int instanceCount, int modelCount, int drawCount) {
drawInstanceIndex.ensureCapacity(instanceCount);
lastFrameVisibility.ensureCapacity(MoreMath.ceilingDiv(instanceCount, 32));
lastFrameVisibility.ensureCapacity(objectStorage.capacity());
boundingSpheres.ensureCapacity(objectStorage.capacity() * 32L);
model.ensureCapacity(modelCount);
draw.ensureCapacity(drawCount);
final long ptr = multiBindBlock.ptr();
MemoryUtil.memPutInt(ptr + BOUNDING_SPHERES_HANDLE_OFFSET, boundingSpheres.handle());
MemoryUtil.memPutInt(ptr + LAST_FRAME_VISIBILITY_HANDLE_OFFSET, lastFrameVisibility.handle());
MemoryUtil.memPutInt(ptr + PAGE_FRAME_DESCRIPTOR_HANDLE_OFFSET, objectStorage.frameDescriptorBuffer.handle());
MemoryUtil.memPutInt(ptr + INSTANCE_HANDLE_OFFSET, objectStorage.objectBuffer.handle());
@ -96,7 +101,8 @@ public class IndirectBuffers {
MemoryUtil.memPutInt(ptr + MODEL_HANDLE_OFFSET, model.handle());
MemoryUtil.memPutInt(ptr + DRAW_HANDLE_OFFSET, draw.handle());
MemoryUtil.memPutAddress(ptr + LAST_FRAME_VISIBILITY_SIZE_OFFSET, INT_SIZE * MoreMath.ceilingDiv(instanceCount, 32));
MemoryUtil.memPutAddress(ptr + BOUNDING_SPHERES_SIZE_OFFSET, 16L * objectStorage.capacity() * 32);
MemoryUtil.memPutAddress(ptr + LAST_FRAME_VISIBILITY_SIZE_OFFSET, INT_SIZE * objectStorage.capacity());
MemoryUtil.memPutAddress(ptr + PAGE_FRAME_DESCRIPTOR_SIZE_OFFSET, objectStorage.frameDescriptorBuffer.capacity());
MemoryUtil.memPutAddress(ptr + INSTANCE_SIZE_OFFSET, objectStorage.objectBuffer.capacity());
MemoryUtil.memPutAddress(ptr + DRAW_INSTANCE_INDEX_SIZE_OFFSET, INT_SIZE * instanceCount);
@ -105,23 +111,23 @@ public class IndirectBuffers {
}
public void bindForCullPassOne() {
multiBind(0, 5);
multiBind(0, 6);
}
public void bindForCullPassTwo() {
multiBind(0, 5);
multiBind(0, 6);
}
public void bindForApply() {
multiBind(4, 2);
multiBind(5, 2);
}
public void bindForModelReset() {
multiBind(4, 1);
multiBind(5, 1);
}
public void bindForDraw() {
multiBind(2, 4);
multiBind(3, 4);
GlBufferType.DRAW_INDIRECT_BUFFER.bind(draw.handle());
}
@ -129,7 +135,7 @@ public class IndirectBuffers {
* Bind all buffers except the draw command buffer.
*/
public void bindForCrumbling() {
multiBind(3, 3);
multiBind(4, 3);
}
private void multiBind(int base, int count) {

View File

@ -23,7 +23,6 @@ import dev.engine_room.flywheel.backend.compile.IndirectPrograms;
import dev.engine_room.flywheel.backend.engine.InstancerKey;
import dev.engine_room.flywheel.backend.engine.MaterialRenderState;
import dev.engine_room.flywheel.backend.engine.MeshPool;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.backend.gl.GlCompat;
import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
import dev.engine_room.flywheel.lib.math.MoreMath;
@ -44,7 +43,6 @@ public class IndirectCullingGroup<I extends Instance> {
private final IndirectPrograms programs;
private final GlProgram earlyCull;
private final GlProgram lateCull;
private boolean needsDrawBarrier;
private boolean needsDrawSort;
@ -58,7 +56,6 @@ public class IndirectCullingGroup<I extends Instance> {
this.programs = programs;
earlyCull = programs.getCullingProgram(instanceType);
lateCull = programs.getCullPassTwoProgram(instanceType);
}
public void flushInstancers() {
@ -113,7 +110,6 @@ public class IndirectCullingGroup<I extends Instance> {
return;
}
Uniforms.bindAll();
earlyCull.bind();
buffers.bindForCullPassOne();
@ -125,9 +121,6 @@ public class IndirectCullingGroup<I extends Instance> {
return;
}
Uniforms.bindAll();
lateCull.bind();
buffers.bindForCullPassTwo();
glDispatchCompute(buffers.objectStorage.capacity(), 1, 1);
}

View File

@ -132,6 +132,9 @@ public class IndirectDrawManager extends DrawManager<IndirectInstancer<?>> {
GlTextureUnit.T0.makeActive();
GlStateManager._bindTexture(depthPyramid.pyramidTextureId);
programs.getCullPassTwoProgram()
.bind();
for (var group1 : cullingGroups.values()) {
group1.dispatchCullPassTwo();
}

View File

@ -2,7 +2,10 @@ package dev.engine_room.flywheel.backend.engine.indirect;
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 org.lwjgl.system.MemoryUtil;
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<I extends Instance> extends AbstractInstancer<I>
private final List<IndirectDraw> associatedDraws = new ArrayList<>();
private final Vector4fc boundingSphere;
private final AtomicBitSet changedPages = new AtomicBitSet();
private final AtomicReference<InstancePage<I>[]> 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,198 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
boundingSphere = key.model().boundingSphere();
}
@Override
public void setIndexChanged(int index) {
changedPages.set(ObjectStorage.objectIndex2PageIndex(index));
@SuppressWarnings("unchecked")
private static <I extends Instance> InstancePage<I>[] pageArray(int length) {
return new InstancePage[length];
}
@Override
protected void setRangeChanged(int start, int end) {
super.setRangeChanged(start, end);
@SuppressWarnings("unchecked")
private static <I extends Instance> I[] instanceArray() {
return (I[]) new Instance[ObjectStorage.PAGE_SIZE];
}
changedPages.set(ObjectStorage.objectIndex2PageIndex(start), ObjectStorage.objectIndex2PageIndex(end) + 1);
@SuppressWarnings("unchecked")
private static <I extends Instance> InstanceHandleImpl<I>[] handleArray() {
return new InstanceHandleImpl[ObjectStorage.PAGE_SIZE];
}
private static final class InstancePage<I extends Instance> implements InstanceHandleImpl.State<I> {
private final IndirectInstancer<I> 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<I>[] handles;
/**
* A bitset describing which indices in the instances/handles arrays contain live instances.
*/
private final AtomicInteger valid;
private InstancePage(IndirectInstancer<I> 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<I> 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<I> setChanged(int index) {
parent.contentsChanged.set(pageNo);
return this;
}
@Override
public InstanceHandleImpl.State<I> setDeleted(int index) {
int localIndex = index % ObjectStorage.PAGE_SIZE;
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);
return InstanceHandleImpl.Deleted.instance();
}
}
}
@Override
public InstanceHandleImpl.State<I> setVisible(InstanceHandleImpl<I> handle, int index, boolean visible) {
if (visible) {
return this;
}
int localIndex = index % ObjectStorage.PAGE_SIZE;
return new InstanceHandleImpl.Hidden<>(parent.recreate, instances[localIndex]);
}
/**
* Only call this on 2 pages that are mergeable.
*
* @param other The page to take instances from.
*/
private void takeFrom(InstancePage<I> 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));
// 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 +262,32 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
}
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 +301,22 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
}
public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) {
if (changedPages.isEmpty()) {
if (contentsChanged.isEmpty()) {
return;
}
int numPages = mapping.pageCount();
var instanceCount = instances.size();
for (int page = changedPages.nextSetBit(0); page >= 0 && page < numPages; page = changedPages.nextSetBit(page + 1)) {
int startObject = ObjectStorage.pageIndex2ObjectIndex(page);
if (startObject >= instanceCount) {
break;
}
int endObject = Math.min(instanceCount, ObjectStorage.pageIndex2ObjectIndex(page + 1));
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;
long baseByte = mapping.page2ByteOffset(page);
long size = (endObject - startObject) * instanceStride;
if (baseByte < 0) {
// This page is not mapped to the VBO.
continue;
}
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 +327,10 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
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 +339,49 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
// 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 +404,135 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
public int local2GlobalInstanceIndex(int instanceIndex) {
return mapping.objectIndex2GlobalIndex(instanceIndex);
}
@Override
public I createInstance() {
var handle = new InstanceHandleImpl<I>(null);
I instance = type.create(handle);
addInner(instance, handle);
return instance;
}
public InstanceHandleImpl.State<I> revealInstance(InstanceHandleImpl<I> 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<I>) 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<I> 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<I>) {
handle.state = new InstanceHandleImpl.Hidden<>(recreate, instance);
}
}
private void addInner(I instance, InstanceHandleImpl<I> 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<I>[] 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();
}
}

View File

@ -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 INITIAL_PAGES_ALLOCATED = 4;
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 class ObjectStorage extends AbstractArena {
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 class ObjectStorage extends AbstractArena {
@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 @@ public class ObjectStorage extends AbstractArena {
}
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 class ObjectStorage extends AbstractArena {
}
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 ObjectStorage extends AbstractArena {
private static final int[] EMPTY_ALLOCATION = new int[0];
private int[] pages = EMPTY_ALLOCATION;
private int modelIndex = -1;
private int objectCount = 0;
/**
* Adjust this allocation to the given model index and object count.
*
* <p>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.
*
* @param modelIndex The model index the objects in this allocation are associated with.
* @param objectCount The number of objects in this allocation.
*/
public void update(int modelIndex, int objectCount) {
boolean incremental = this.modelIndex == modelIndex;
if (incremental && objectCount == this.objectCount) {
// Nothing will change.
public void updatePage(int index, int modelIndex, int validBits) {
if (validBits == 0) {
holePunch(index);
return;
}
var frame = pages[index];
ObjectStorage.this.needsUpload = true;
if (frame == INVALID_PAGE) {
// Un-holed punch.
frame = unHolePunch(index);
}
this.modelIndex = modelIndex;
this.objectCount = objectCount;
ObjectStorage.this.set(frame, modelIndex, validBits);
}
/**
* Free a page on the inside of the mapping, maintaining the same virtual mapping size.
*
* @param index The index of the page to free.
*/
public void holePunch(int index) {
ObjectStorage.this.free(pages[index]);
pages[index] = INVALID_PAGE;
}
/**
* 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 class ObjectStorage extends AbstractArena {
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 class ObjectStorage extends AbstractArena {
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) {

View File

@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable;
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.backend.gl.buffer.GlBufferUsage;
import dev.engine_room.flywheel.lib.math.MoreMath;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I> {
public class InstancedInstancer<I extends Instance> extends BaseInstancer<I> {
private final int instanceStride;
private final InstanceWriter<I> writer;
@ -109,6 +109,60 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
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;

View File

@ -43,6 +43,14 @@ public class AtomicBitSet {
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);

View File

@ -2,16 +2,17 @@
// A few of these could be combined.
// Per culling group
#define _FLW_LAST_FRAME_VISIBILITY_BUFFER_BINDING 0// cull1, cull2
#define _FLW_PAGE_FRAME_DESCRIPTOR_BUFFER_BINDING 1// cull1, cull2
#define _FLW_INSTANCE_BUFFER_BINDING 2// cull1, cull2, draw
#define _FLW_DRAW_INSTANCE_INDEX_BUFFER_BINDING 3// cull1, cull2, draw
#define _FLW_MODEL_BUFFER_BINDING 4// cull1, cull2, apply
#define _FLW_DRAW_BUFFER_BINDING 5// apply, draw
#define _FLW_BOUNDING_SPHERE_BINDING 0// cull1, cull2
#define _FLW_LAST_FRAME_VISIBILITY_BUFFER_BINDING 1// cull1, cull2
#define _FLW_PAGE_FRAME_DESCRIPTOR_BUFFER_BINDING 2// cull1, cull2
#define _FLW_INSTANCE_BUFFER_BINDING 3// cull1, cull2, draw
#define _FLW_DRAW_INSTANCE_INDEX_BUFFER_BINDING 4// cull1, cull2, draw
#define _FLW_MODEL_BUFFER_BINDING 5// cull1, cull2, apply
#define _FLW_DRAW_BUFFER_BINDING 6// apply, draw
// Global to the engine
#define _FLW_LIGHT_LUT_BUFFER_BINDING 6
#define _FLW_LIGHT_SECTIONS_BUFFER_BINDING 7
#define _FLW_LIGHT_LUT_BUFFER_BINDING 7
#define _FLW_LIGHT_SECTIONS_BUFFER_BINDING 8
#define _FLW_MATRIX_BUFFER_BINDING 8
#define _FLW_MATRIX_BUFFER_BINDING 9

View File

@ -1,31 +0,0 @@
layout(local_size_x = 16, local_size_y = 16) in;
layout(binding = 0, r32f) uniform writeonly image2D outImage;
layout(binding = 1) uniform sampler2D inImage;
uniform vec2 oneOverImageSize;
uniform int lod;
uniform int useMin = 0;
void main() {
uvec2 pos = gl_GlobalInvocationID.xy;
// Map the output texel to an input texel. Properly do the division because generating mip0 maps from the actual
// full resolution depth buffer and the aspect ratio may be different from our Po2 pyramid.
ivec2 samplePos = ivec2(floor(vec2(pos) * vec2(textureSize(inImage, lod)) * oneOverImageSize));
float depth01 = texelFetchOffset(inImage, samplePos, lod, ivec2(0, 1)).r;
float depth11 = texelFetchOffset(inImage, samplePos, lod, ivec2(1, 1)).r;
float depth10 = texelFetchOffset(inImage, samplePos, lod, ivec2(1, 0)).r;
float depth00 = texelFetchOffset(inImage, samplePos, lod, ivec2(0, 0)).r;
float depth;
if (useMin == 0) {
depth = max(max(depth00, depth01), max(depth10, depth11));
} else {
depth = min(min(depth00, depth01), min(depth10, depth11));
}
imageStore(outImage, ivec2(pos), vec4(depth));
}

View File

@ -7,15 +7,14 @@
layout(local_size_x = 32) in;
layout(std430, binding = _FLW_BOUNDING_SPHERE_BINDING) restrict writeonly buffer BoundingSphereBuffer {
vec4 _flw_boundingSpheres[];
};
layout(std430, binding = _FLW_DRAW_INSTANCE_INDEX_BUFFER_BINDING) restrict writeonly buffer DrawIndexBuffer {
uint _flw_drawIndices[];
};
// High 6 bits for the number of instances in the page.
const uint _FLW_PAGE_COUNT_OFFSET = 26u;
// Bottom 26 bits for the model index.
const uint _FLW_MODEL_INDEX_MASK = 0x3FFFFFF;
layout(std430, binding = _FLW_PAGE_FRAME_DESCRIPTOR_BUFFER_BINDING) restrict readonly buffer PageFrameDescriptorBuffer {
uint _flw_pageFrameDescriptors[];
};
@ -61,39 +60,36 @@ bool _flw_isVisible(uint instanceIndex, uint modelIndex) {
transformBoundingSphere(_flw_matrices[matrixIndex].pose, center, radius);
}
_flw_boundingSpheres[instanceIndex] = vec4(center, radius);
return _flw_testSphere(center, radius);
}
// TODO: There's an opportunity here to write out the transformed bounding spheres to a buffer and use them in pass 2,
// instead of pulling the entire instance again. It would save a lot of memory bandwidth and matrix multiplications in
// pass 2, but it would also be a good bit of writes in pass 1. It's worth investigating, but it would be nice to have
// nsight trace working to be more sure.
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)) {
return;
}
uint pageVisibility = _flw_visibility[pageIndex];
uint pageVisibility = _flw_visibility[gl_WorkGroupID.x];
bool visibleLastFrame = (_flw_visibility[gl_WorkGroupID.x] & (1u << gl_LocalInvocationID.x)) != 0u;
if ((pageVisibility & (1u << gl_LocalInvocationID.x)) != 0u) {
if (visibleLastFrame) {
// This instance was visibile last frame, it should be rendered early.
uint localIndex = atomicAdd(_flw_models[modelIndex].instanceCount, 1);
uint targetIndex = _flw_models[modelIndex].baseInstance + localIndex;

View File

@ -2,7 +2,6 @@
#include "flywheel:internal/indirect/model_descriptor.glsl"
#include "flywheel:internal/uniforms/uniforms.glsl"
#include "flywheel:util/matrix.glsl"
#include "flywheel:internal/indirect/matrices.glsl"
#include "flywheel:internal/indirect/dispatch.glsl"
layout(local_size_x = 32) in;
@ -11,12 +10,9 @@ layout(std430, binding = _FLW_DRAW_INSTANCE_INDEX_BUFFER_BINDING) restrict write
uint _flw_drawIndices[];
};
// High 6 bits for the number of instances in the page.
const uint _FLW_PAGE_COUNT_OFFSET = 26u;
// Bottom 26 bits for the model index.
const uint _FLW_MODEL_INDEX_MASK = 0x3FFFFFF;
layout(std430, binding = _FLW_BOUNDING_SPHERE_BINDING) restrict readonly buffer BoundingSphereBuffer {
vec4 _flw_boundingSpheres[];
};
layout(std430, binding = _FLW_PAGE_FRAME_DESCRIPTOR_BUFFER_BINDING) restrict readonly buffer PageFrameDescriptorBuffer {
uint _flw_pageFrameDescriptors[];
@ -30,10 +26,6 @@ layout(std430, binding = _FLW_MODEL_BUFFER_BINDING) restrict buffer ModelBuffer
ModelDescriptor _flw_models[];
};
layout(std430, binding = _FLW_MATRIX_BUFFER_BINDING) restrict readonly buffer MatrixBuffer {
Matrices _flw_matrices[];
};
layout(binding = 0) uniform sampler2D _flw_depthPyramid;
bool projectSphere(vec3 c, float r, float znear, float P00, float P11, out vec4 aabb) {
@ -89,6 +81,10 @@ bool _flw_hizTest(vec3 center, float radius) {
ivec4 bounds = ivec4(aabb * vec4(levelSizePair));
// 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);
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;
@ -109,21 +105,11 @@ bool _flw_hizTest(vec3 center, float radius) {
return true;
}
bool _flw_isVisible(uint instanceIndex, uint modelIndex) {
uint matrixIndex = _flw_models[modelIndex].matrixIndex;
BoundingSphere sphere = _flw_models[modelIndex].boundingSphere;
bool _flw_isVisible(uint instanceIndex) {
vec4 boundingSphere = _flw_boundingSpheres[instanceIndex];
vec3 center;
float radius;
_flw_unpackBoundingSphere(sphere, center, radius);
FlwInstance instance = _flw_unpackInstance(instanceIndex);
flw_transformBoundingSphere(instance, center, radius);
if (matrixIndex > 0) {
transformBoundingSphere(_flw_matrices[matrixIndex].pose, center, radius);
}
vec3 center = boundingSphere.xyz;
float radius = boundingSphere.w;
bool visible = _flw_testSphere(center, radius);
@ -135,26 +121,24 @@ 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;
bool visible = _flw_isVisible(instanceIndex, modelIndex);
bool visibleLastFrame = (_flw_visibility[pageIndex] & (1u << gl_LocalInvocationID.x)) != 0u;
bool visible = _flw_isVisible(instanceIndex);
bool visibleLastFrame = (_flw_visibility[gl_WorkGroupID.x] & (1u << gl_LocalInvocationID.x)) != 0u;
if (visible && !visibleLastFrame) {
uint localIndex = atomicAdd(_flw_models[modelIndex].instanceCount, 1);
@ -166,6 +150,6 @@ void main() {
uvec4 visibility = subgroupBallot(visible);
if (subgroupElect()) {
_flw_visibility[pageIndex] = visibility.x;
_flw_visibility[gl_WorkGroupID.x] = visibility.x;
}
}

View File

@ -24,4 +24,12 @@ public interface FlwLibLink {
void compileModelPart(ModelPart part, PoseStack.Pose pose, VertexConsumer consumer, int light, int overlay, float red, float green, float blue, float alpha);
Deque<PoseStack.Pose> getPoseStack(PoseStack stack);
boolean isIrisLoaded();
boolean isOptifineInstalled();
boolean isShaderPackInUse();
boolean isRenderingShadowPass();
}

View File

@ -1,13 +1,11 @@
package dev.engine_room.flywheel.lib.internal;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import dev.engine_room.flywheel.api.internal.DependencyInjection;
import dev.engine_room.flywheel.lib.model.baked.BakedModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.BlockModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.MultiBlockModelBuilder;
import dev.engine_room.flywheel.lib.util.ShadersModHandler;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
@ -29,7 +27,4 @@ public interface FlwLibXplat {
BlockModelBuilder createBlockModelBuilder(BlockState state);
MultiBlockModelBuilder createMultiBlockModelBuilder(BlockAndTintGetter level, Iterable<BlockPos> positions);
@Nullable
ShadersModHandler.InternalHandler createIrisHandler();
}

View File

@ -3,7 +3,7 @@ package dev.engine_room.flywheel.lib.material;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.material.LightShader;
public class LightShaders {
public final class LightShaders {
public static final LightShader SMOOTH_WHEN_EMBEDDED = new SimpleLightShader(Flywheel.rl("light/smooth_when_embedded.glsl"));
public static final LightShader SMOOTH = new SimpleLightShader(Flywheel.rl("light/smooth.glsl"));
public static final LightShader FLAT = new SimpleLightShader(Flywheel.rl("light/flat.glsl"));

View File

@ -178,6 +178,7 @@ public class SimpleMaterial implements Material {
shaders = material.shaders();
fog = material.fog();
cutout = material.cutout();
light = material.light();
texture = material.texture();
blur = material.blur();
mipmap = material.mipmap();

View File

@ -1,128 +0,0 @@
package dev.engine_room.flywheel.lib.util;
import java.lang.reflect.Field;
import java.util.function.BooleanSupplier;
import org.jetbrains.annotations.ApiStatus;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import dev.engine_room.flywheel.lib.internal.FlwLibXplat;
public final class ShadersModHandler {
private static final String OPTIFINE_ROOT_PACKAGE = "net.optifine";
private static final boolean IS_IRIS_LOADED;
private static final boolean IS_OPTIFINE_INSTALLED;
private static final InternalHandler INTERNAL_HANDLER;
static {
InternalHandler irisHandler = FlwLibXplat.INSTANCE
.createIrisHandler();
IS_IRIS_LOADED = irisHandler != null;
Package optifinePackage = Package.getPackage(OPTIFINE_ROOT_PACKAGE);
IS_OPTIFINE_INSTALLED = optifinePackage != null;
// OptiFine and Iris/Oculus are assumed to be mutually exclusive
if (IS_IRIS_LOADED) {
FlwLibLink.INSTANCE.getLogger().debug("Iris detected.");
INTERNAL_HANDLER = irisHandler;
} else if (IS_OPTIFINE_INSTALLED) {
FlwLibLink.INSTANCE.getLogger().debug("OptiFine detected.");
INTERNAL_HANDLER = new OptifineHandler();
} else {
FlwLibLink.INSTANCE.getLogger().debug("No shaders mod detected.");
INTERNAL_HANDLER = new InternalHandler() {};
}
}
private ShadersModHandler() {
}
public static boolean isIrisLoaded() {
return IS_IRIS_LOADED;
}
public static boolean isOptifineInstalled() {
return IS_OPTIFINE_INSTALLED;
}
public static boolean isShaderPackInUse() {
return INTERNAL_HANDLER.isShaderPackInUse();
}
public static boolean isRenderingShadowPass() {
return INTERNAL_HANDLER.isRenderingShadowPass();
}
@ApiStatus.Internal
public static void init() {
}
@ApiStatus.Internal
public interface InternalHandler {
default boolean isShaderPackInUse() {
return false;
}
default boolean isRenderingShadowPass() {
return false;
}
}
// evil reflection
private static class OptifineHandler implements InternalHandler {
private final BooleanSupplier shadersEnabledSupplier;
private final BooleanSupplier shadowPassSupplier;
private OptifineHandler() {
shadersEnabledSupplier = createShadersEnabledSupplier();
shadowPassSupplier = createShadowPassSupplier();
}
@Override
public boolean isShaderPackInUse() {
return shadersEnabledSupplier.getAsBoolean();
}
@Override
public boolean isRenderingShadowPass() {
return shadowPassSupplier.getAsBoolean();
}
private static BooleanSupplier createShadersEnabledSupplier() {
try {
Class<?> ofShaders = Class.forName("net.optifine.shaders.Shaders");
Field field = ofShaders.getDeclaredField("shaderPackLoaded");
field.setAccessible(true);
return () -> {
try {
return field.getBoolean(null);
} catch (IllegalAccessException e) {
return false;
}
};
} catch (Exception e) {
return () -> false;
}
}
private static BooleanSupplier createShadowPassSupplier() {
try {
Class<?> ofShaders = Class.forName("net.optifine.shaders.Shaders");
Field field = ofShaders.getDeclaredField("isShadowPass");
field.setAccessible(true);
return () -> {
try {
return field.getBoolean(null);
} catch (IllegalAccessException e) {
return false;
}
};
} catch (Exception e) {
return () -> false;
}
}
}
}

View File

@ -0,0 +1,19 @@
package dev.engine_room.flywheel.lib.util;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
public final class ShadersModHelper {
public static final boolean IS_IRIS_LOADED = FlwLibLink.INSTANCE.isIrisLoaded();
public static final boolean IS_OPTIFINE_INSTALLED = FlwLibLink.INSTANCE.isOptifineInstalled();
private ShadersModHelper() {
}
public static boolean isShaderPackInUse() {
return FlwLibLink.INSTANCE.isShaderPackInUse();
}
public static boolean isRenderingShadowPass() {
return FlwLibLink.INSTANCE.isRenderingShadowPass();
}
}

View File

@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.backend.FlwBackend;
import dev.engine_room.flywheel.impl.registry.IdRegistryImpl;
import dev.engine_room.flywheel.lib.util.ShadersModHandler;
import dev.engine_room.flywheel.vanilla.VanillaVisuals;
public final class FlwImpl {
@ -21,7 +20,6 @@ public final class FlwImpl {
BackendManagerImpl.init();
// lib
ShadersModHandler.init();
// backend
FlwBackend.init(FlwConfig.INSTANCE.backendConfig());

View File

@ -6,9 +6,15 @@ import net.minecraft.client.multiplayer.ClientLevel;
public interface FlwImplXplat {
FlwImplXplat INSTANCE = DependencyInjection.load(FlwImplXplat.class, "dev.engine_room.flywheel.impl.FlwImplXplatImpl");
boolean isModLoaded(String modId);
void dispatchReloadLevelRendererEvent(ClientLevel level);
String getVersionStr();
FlwConfig getConfig();
boolean useSodium0_6Compat();
boolean useIrisCompat();
}

View File

@ -8,6 +8,8 @@ import org.slf4j.Logger;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.engine_room.flywheel.impl.compat.IrisCompat;
import dev.engine_room.flywheel.impl.compat.OptifineCompat;
import dev.engine_room.flywheel.impl.extension.PoseStackExtension;
import dev.engine_room.flywheel.impl.mixin.ModelPartAccessor;
import dev.engine_room.flywheel.impl.mixin.PoseStackAccessor;
@ -40,4 +42,36 @@ public class FlwLibLinkImpl implements FlwLibLink {
public Deque<PoseStack.Pose> getPoseStack(PoseStack stack) {
return ((PoseStackAccessor) stack).flywheel$getPoseStack();
}
@Override
public boolean isIrisLoaded() {
return IrisCompat.ACTIVE;
}
@Override
public boolean isOptifineInstalled() {
return OptifineCompat.IS_INSTALLED;
}
@Override
public boolean isShaderPackInUse() {
if (IrisCompat.ACTIVE) {
return IrisCompat.isShaderPackInUse();
} else if (OptifineCompat.IS_INSTALLED) {
return OptifineCompat.isShaderPackInUse();
} else {
return false;
}
}
@Override
public boolean isRenderingShadowPass() {
if (IrisCompat.ACTIVE) {
return IrisCompat.isRenderingShadowPass();
} else if (OptifineCompat.IS_INSTALLED) {
return OptifineCompat.isRenderingShadowPass();
} else {
return false;
}
}
}

View File

@ -0,0 +1,18 @@
package dev.engine_room.flywheel.impl.compat;
import dev.engine_room.flywheel.impl.FlwImplXplat;
public enum CompatMod {
EMBEDDIUM("embeddium"),
IRIS("iris"),
OCULUS("oculus"),
SODIUM("sodium");
public final String id;
public final boolean isLoaded;
CompatMod(String modId) {
id = modId;
isLoaded = FlwImplXplat.INSTANCE.isModLoaded(modId);
}
}

View File

@ -0,0 +1,46 @@
package dev.engine_room.flywheel.impl.compat;
import dev.engine_room.flywheel.impl.FlwImpl;
import dev.engine_room.flywheel.impl.FlwImplXplat;
import net.irisshaders.iris.api.v0.IrisApi;
public final class IrisCompat {
public static final boolean ACTIVE = FlwImplXplat.INSTANCE.useIrisCompat();
static {
if (ACTIVE) {
FlwImpl.LOGGER.debug("Detected Iris");
}
}
private IrisCompat() {
}
public static boolean isShaderPackInUse() {
if (!ACTIVE) {
return false;
}
return Internals.isShaderPackInUse();
}
public static boolean isRenderingShadowPass() {
if (!ACTIVE) {
return false;
}
return Internals.isRenderingShadowPass();
}
private static final class Internals {
static boolean isShaderPackInUse() {
return IrisApi.getInstance()
.isShaderPackInUse();
}
static boolean isRenderingShadowPass() {
return IrisApi.getInstance()
.isRenderingShadowPass();
}
}
}

View File

@ -0,0 +1,73 @@
package dev.engine_room.flywheel.impl.compat;
import java.lang.reflect.Field;
import org.jetbrains.annotations.Nullable;
import dev.engine_room.flywheel.impl.FlwImpl;
public final class OptifineCompat {
private static final String OPTIFINE_ROOT_PACKAGE = "net.optifine";
public static final boolean IS_INSTALLED;
@Nullable
private static final Field SHADER_PACK_LOADED_FIELD;
@Nullable
private static final Field IS_SHADOW_PASS_FIELD;
static {
Field shaderPackLoadedField = null;
Field isShadowPassField = null;
Package optifinePackage = Package.getPackage(OPTIFINE_ROOT_PACKAGE);
IS_INSTALLED = optifinePackage != null;
if (IS_INSTALLED) {
try {
Class<?> shadersClass = Class.forName("net.optifine.shaders.Shaders");
shaderPackLoadedField = shadersClass.getDeclaredField("shaderPackLoaded");
shaderPackLoadedField.setAccessible(true);
isShadowPassField = shadersClass.getDeclaredField("isShadowPass");
isShadowPassField.setAccessible(true);
} catch (Exception e) {
FlwImpl.LOGGER.debug("Failed to access OptiFine internals", e);
}
}
SHADER_PACK_LOADED_FIELD = shaderPackLoadedField;
IS_SHADOW_PASS_FIELD = isShadowPassField;
}
static {
if (IS_INSTALLED) {
FlwImpl.LOGGER.debug("Detected OptiFine");
}
}
private OptifineCompat() {
}
public static boolean isShaderPackInUse() {
if (SHADER_PACK_LOADED_FIELD == null) {
return false;
}
try {
return SHADER_PACK_LOADED_FIELD.getBoolean(null);
} catch (IllegalAccessException e) {
return false;
}
}
public static boolean isRenderingShadowPass() {
if (IS_SHADOW_PASS_FIELD == null) {
return false;
}
try {
return IS_SHADOW_PASS_FIELD.getBoolean(null);
} catch (IllegalAccessException e) {
return false;
}
}
}

View File

@ -0,0 +1,61 @@
package dev.engine_room.flywheel.impl.compat;
import org.jetbrains.annotations.Nullable;
import dev.engine_room.flywheel.api.visualization.BlockEntityVisualizer;
import dev.engine_room.flywheel.impl.FlwImpl;
import dev.engine_room.flywheel.impl.FlwImplXplat;
import dev.engine_room.flywheel.lib.visualization.VisualizationHelper;
import net.caffeinemc.mods.sodium.api.blockentity.BlockEntityRenderHandler;
import net.caffeinemc.mods.sodium.api.blockentity.BlockEntityRenderPredicate;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
public final class SodiumCompat {
public static final boolean USE_0_6_COMPAT = FlwImplXplat.INSTANCE.useSodium0_6Compat();
static {
if (USE_0_6_COMPAT) {
FlwImpl.LOGGER.debug("Detected Sodium 0.6");
}
}
private SodiumCompat() {
}
@Nullable
public static <T extends BlockEntity> Object onSetBlockEntityVisualizer(BlockEntityType<T> type, @Nullable BlockEntityVisualizer<? super T> oldVisualizer, @Nullable BlockEntityVisualizer<? super T> newVisualizer, @Nullable Object predicate) {
if (!USE_0_6_COMPAT) {
return null;
}
if (oldVisualizer == null && newVisualizer != null) {
if (predicate != null) {
throw new IllegalArgumentException("Sodium predicate must be null when old visualizer is null");
}
return Internals.addPredicate(type);
} else if (oldVisualizer != null && newVisualizer == null) {
if (predicate == null) {
throw new IllegalArgumentException("Sodium predicate must not be null when old visualizer is not null");
}
Internals.removePredicate(type, predicate);
return null;
}
return predicate;
}
private static final class Internals {
static <T extends BlockEntity> Object addPredicate(BlockEntityType<T> type) {
BlockEntityRenderPredicate<T> predicate = (getter, pos, be) -> VisualizationHelper.tryAddBlockEntity(be);
BlockEntityRenderHandler.instance().addRenderPredicate(type, predicate);
return predicate;
}
static <T extends BlockEntity> void removePredicate(BlockEntityType<T> type, Object predicate) {
BlockEntityRenderHandler.instance().removeRenderPredicate(type, (BlockEntityRenderPredicate<T>) predicate);
}
}
}

View File

@ -5,6 +5,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import dev.engine_room.flywheel.api.visualization.BlockEntityVisualizer;
import dev.engine_room.flywheel.impl.compat.SodiumCompat;
import dev.engine_room.flywheel.impl.extension.BlockEntityTypeExtension;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
@ -12,8 +13,13 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
@Mixin(BlockEntityType.class)
abstract class BlockEntityTypeMixin<T extends BlockEntity> implements BlockEntityTypeExtension<T> {
@Unique
@Nullable
private BlockEntityVisualizer<? super T> flywheel$visualizer;
@Unique
@Nullable
private Object flywheel$sodiumPredicate;
@Override
@Nullable
public BlockEntityVisualizer<? super T> flywheel$getVisualizer() {
@ -22,6 +28,10 @@ abstract class BlockEntityTypeMixin<T extends BlockEntity> implements BlockEntit
@Override
public void flywheel$setVisualizer(@Nullable BlockEntityVisualizer<? super T> visualizer) {
this.flywheel$visualizer = visualizer;
if (SodiumCompat.USE_0_6_COMPAT) {
flywheel$sodiumPredicate = SodiumCompat.onSetBlockEntityVisualizer((BlockEntityType<T>) (Object) this, flywheel$visualizer, visualizer, flywheel$sodiumPredicate);
}
flywheel$visualizer = visualizer;
}
}

View File

@ -12,6 +12,7 @@ import net.minecraft.world.entity.EntityType;
@Mixin(EntityType.class)
abstract class EntityTypeMixin<T extends Entity> implements EntityTypeExtension<T> {
@Unique
@Nullable
private EntityVisualizer<? super T> flywheel$visualizer;
@Override
@ -22,6 +23,6 @@ abstract class EntityTypeMixin<T extends Entity> implements EntityTypeExtension<
@Override
public void flywheel$setVisualizer(@Nullable EntityVisualizer<? super T> visualizer) {
this.flywheel$visualizer = visualizer;
flywheel$visualizer = visualizer;
}
}

View File

@ -125,8 +125,8 @@ abstract class LevelRendererMixin {
}
@Inject(method = "renderEntity", at = @At("HEAD"), cancellable = true)
private void flywheel$decideNotToRenderEntity(Entity pEntity, double pCamX, double pCamY, double pCamZ, float pPartialTick, PoseStack pPoseStack, MultiBufferSource pBufferSource, CallbackInfo ci) {
if (VisualizationManager.supportsVisualization(pEntity.level()) && VisualizationHelper.skipVanillaRender(pEntity)) {
private void flywheel$decideNotToRenderEntity(Entity entity, double camX, double camY, double camZ, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, CallbackInfo ci) {
if (VisualizationManager.supportsVisualization(entity.level()) && VisualizationHelper.skipVanillaRender(entity)) {
ci.cancel();
}
}

View File

@ -11,7 +11,6 @@ import net.minecraft.world.entity.EntityType;
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

View File

@ -7,26 +7,6 @@ import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.entity.BlockEntityType;
/**
* TODO:
* <table>
* <tr><td>{@link BlockEntityType#SIGN}</td><td> {@link net.minecraft.client.renderer.blockentity.SignRenderer SignRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#PISTON}</td><td> {@link net.minecraft.client.renderer.blockentity.PistonHeadRenderer PistonHeadRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#CONDUIT}</td><td> {@link net.minecraft.client.renderer.blockentity.ConduitRenderer ConduitRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#ENCHANTING_TABLE}</td><td> {@link net.minecraft.client.renderer.blockentity.EnchantTableRenderer EnchantTableRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#LECTERN}</td><td> {@link net.minecraft.client.renderer.blockentity.LecternRenderer LecternRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#MOB_SPAWNER}</td><td> {@link net.minecraft.client.renderer.blockentity.SpawnerRenderer SpawnerRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#BED}</td><td> {@link net.minecraft.client.renderer.blockentity.BedRenderer BedRenderer}</td></tr>
* <tr><td>^^ Interesting - Major vv</td></tr>
* <tr><td>{@link BlockEntityType#END_PORTAL}</td><td> {@link net.minecraft.client.renderer.blockentity.TheEndPortalRenderer TheEndPortalRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#END_GATEWAY}</td><td> {@link net.minecraft.client.renderer.blockentity.TheEndGatewayRenderer TheEndGatewayRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#BEACON}</td><td> {@link net.minecraft.client.renderer.blockentity.BeaconRenderer BeaconRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#SKULL}</td><td> {@link net.minecraft.client.renderer.blockentity.SkullBlockRenderer SkullBlockRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#BANNER}</td><td> {@link net.minecraft.client.renderer.blockentity.BannerRenderer BannerRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#STRUCTURE_BLOCK}</td><td> {@link net.minecraft.client.renderer.debug.StructureRenderer StructureRenderer}</td></tr>
* <tr><td>{@link BlockEntityType#CAMPFIRE}</td><td> {@link net.minecraft.client.renderer.blockentity.CampfireRenderer CampfireRenderer}</td></tr>
* </table>
*/
public class VanillaVisuals {
public static void init() {
builder(BlockEntityType.CHEST)

View File

@ -0,0 +1,38 @@
// https://github.com/CaffeineMC/sodium-fabric/blob/e7643f4544f61180ed2f0ff4952d7daa2c1feaf4/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java
// PolyForm Shield License 1.0.0
package net.caffeinemc.mods.sodium.api.blockentity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Experimental
@ApiStatus.AvailableSince("0.6.0")
public interface BlockEntityRenderHandler {
BlockEntityRenderHandler INSTANCE = null;
static BlockEntityRenderHandler instance() {
return INSTANCE;
}
/**
* Adds a predicate to determine if a block entity should be rendered.
*
* <p>Upon chunk bake, block entities of the given type will have {@code shouldRender} evaluated.
* <br>If <b>all predicates</b> returns {@code true} (and the block entity has a renderer), the block entity will be
* added to the chunk for future rendering.</p>
* @param type The block entity type to associate the given predicate with.
* @param shouldRender The predicate for the block entity to evaluate.
*/
<T extends BlockEntity> void addRenderPredicate(BlockEntityType<T> type, BlockEntityRenderPredicate<T> shouldRender);
/**
* Removes a predicate added by {@code addRenderPredicate}. <b>It must be the same object that was added.</b>
*
* @param type The block entity type to associate the given predicate with.
* @param shouldRender The predicate to remove.
* @return If the predicate existed and was removed.
*/
<T extends BlockEntity> boolean removeRenderPredicate(BlockEntityType<T> type, BlockEntityRenderPredicate<T> shouldRender);
}

View File

@ -0,0 +1,16 @@
// https://github.com/CaffeineMC/sodium-fabric/blob/e7643f4544f61180ed2f0ff4952d7daa2c1feaf4/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderPredicate.java
// PolyForm Shield License 1.0.0
package net.caffeinemc.mods.sodium.api.blockentity;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Experimental
@ApiStatus.AvailableSince("0.6.0")
@FunctionalInterface
public interface BlockEntityRenderPredicate<T extends BlockEntity> {
boolean shouldRender(BlockGetter blockGetter, BlockPos blockPos, T entity);
}

View File

@ -0,0 +1,114 @@
// https://github.com/IrisShaders/Iris/blob/20be7fc1ff8a48048cb4eb787e1299782bb1caa4/src/main/java/net/irisshaders/iris/api/v0/IrisApi.java
// GNU Lesser General Public License v3.0
package net.irisshaders.iris.api.v0;
/**
* The entry point to the Iris API, major version 0. This is currently the latest
* version of the API.
*
* To access the API, use {@link #getInstance()}.
*/
public interface IrisApi {
/**
* @since API v0.0
*/
static IrisApi getInstance() {
return null;
}
/**
* Gets the minor revision of this API. This is incremented when
* new methods are added without breaking API. Mods can check this
* if they wish to check whether given API calls are available on
* the currently installed Iris version.
*
* @return The current minor revision. Currently, revision 2.
*/
int getMinorApiRevision();
/**
* Checks whether a shader pack is currently in use and being used
* for rendering. If there is no shader pack enabled or a shader
* pack failed to compile and is therefore not in use, this will
* return false.
*
* <p>Mods that need to enable custom workarounds for shaders
* should use this method.
*
* @return Whether shaders are being used for rendering.
* @since {@link #getMinorApiRevision() API v0.0}
*/
boolean isShaderPackInUse();
/**
* Checks whether the shadow pass is currently being rendered.
*
* <p>Generally, mods won't need to call this function for much.
* Mods should be fine with things being rendered multiple times
* each frame from different camera perspectives. Often, there's
* a better approach to fixing bugs than calling this function.
*
* <p>Pretty much the main legitimate use for this function that
* I've seen is in a mod like Immersive Portals, where it has
* very custom culling that doesn't work when the Iris shadow
* pass is active.
*
* <p>Naturally, this function can only return true if
* {@link #isShaderPackInUse()} returns true.
*
* @return Whether Iris is currently rendering the shadow pass.
* @since API v0.0
*/
boolean isRenderingShadowPass();
/**
* Opens the main Iris GUI screen. It's up to Iris to decide
* what this screen is, but generally this is the shader selection
* screen.
*
* This method takes and returns Objects instead of any concrete
* Minecraft screen class to avoid referencing Minecraft classes.
* Nevertheless, the passed parent must either be null, or an
* object that is a subclass of the appropriate {@code Screen}
* class for the given Minecraft version.
*
* @param parent The parent screen, an instance of the appropriate
* {@code Screen} class.
* @return A {@code Screen} class for the main Iris GUI screen.
* @since API v0.0
*/
Object openMainIrisScreenObj(Object parent);
/**
* Gets the language key of the main screen. Currently, this
* is "options.iris.shaderPackSelection".
*
* @return the language key, for use with {@code TranslatableText}
* / {@code TranslatableComponent}
* @since API v0.0
*/
String getMainScreenLanguageKey();
// /**
// * Gets a config object that can edit the Iris configuration.
// * @since API v0.0
// */
// IrisApiConfig getConfig();
// /**
// * Gets a text vertex sink to render into.
// * @param maxQuadCount Maximum amount of quads that will be rendered with this sink
// * @param bufferProvider An IntFunction that can provide a {@code ByteBuffer} with at minimum the bytes provided by the input parameter
// * @since API 0.1
// */
// IrisTextVertexSink createTextVertexSink(int maxQuadCount, IntFunction<ByteBuffer> bufferProvider);
/**
* Gets the sun path rotation used by the current shader pack.
*
* @return The sun path rotation as specified by the shader pack, or 0 if no shader pack is in use.
* @since API v0.2
*/
float getSunPathRotation();
}

View File

@ -10,6 +10,7 @@ plugins {
val api = sourceSets.create("api")
val lib = sourceSets.create("lib")
val backend = sourceSets.create("backend")
val stubs = sourceSets.create("stubs")
val main = sourceSets.getByName("main")
transitiveSourceSets {
@ -26,7 +27,12 @@ transitiveSourceSets {
rootCompile()
compile(api, lib)
}
sourceSet(stubs) {
rootCompile()
}
sourceSet(main) {
// Don't want stubs at runtime
compile(stubs)
implementation(api, lib, backend)
}
@ -35,11 +41,10 @@ transitiveSourceSets {
platform {
commonProject = project(":common")
sources(api, lib, backend, main)
compileWithCommonSourceSets()
setupLoomMod()
compileWithCommonSourceSets(api, lib, backend, stubs, main)
setupLoomMod(api, lib, backend, main)
setupLoomRuns()
setupFatJar()
setupFatJar(api, lib, backend, main)
}
jarSets {
@ -73,10 +78,10 @@ dependencies {
modApi("net.fabricmc.fabric-api:fabric-api:${property("fabric_api_version")}")
modCompileOnly("maven.modrinth:sodium:${property("sodium_version")}")
modCompileOnly("maven.modrinth:iris:${property("iris_version")}")
"forApi"(project(path = ":common", configuration = "commonApiOnly"))
"forLib"(project(path = ":common", configuration = "commonLib"))
"forBackend"(project(path = ":common", configuration = "commonBackend"))
"forStubs"(project(path = ":common", configuration = "commonStubs"))
"forMain"(project(path = ":common", configuration = "commonImpl"))
}

View File

@ -1,9 +1,17 @@
package dev.engine_room.flywheel.impl;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererCallback;
import dev.engine_room.flywheel.impl.compat.CompatMod;
import dev.engine_room.flywheel.impl.compat.FabricSodiumCompat;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.multiplayer.ClientLevel;
public class FlwImplXplatImpl implements FlwImplXplat {
@Override
public boolean isModLoaded(String modId) {
return FabricLoader.getInstance().isModLoaded(modId);
}
@Override
public void dispatchReloadLevelRendererEvent(ClientLevel level) {
ReloadLevelRendererCallback.EVENT.invoker().onReloadLevelRenderer(level);
@ -18,4 +26,14 @@ public class FlwImplXplatImpl implements FlwImplXplat {
public FlwConfig getConfig() {
return FabricFlwConfig.INSTANCE;
}
@Override
public boolean useSodium0_6Compat() {
return FabricSodiumCompat.USE_0_6_COMPAT;
}
@Override
public boolean useIrisCompat() {
return CompatMod.IRIS.isLoaded;
}
}

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.impl;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import dev.engine_room.flywheel.lib.internal.FlwLibXplat;
@ -10,9 +9,6 @@ import dev.engine_room.flywheel.lib.model.baked.FabricBakedModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.FabricBlockModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.FabricMultiBlockModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.MultiBlockModelBuilder;
import dev.engine_room.flywheel.lib.util.ShadersModHandler;
import net.fabricmc.loader.api.FabricLoader;
import net.irisshaders.iris.api.v0.IrisApi;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.client.resources.model.BakedModel;
@ -48,27 +44,4 @@ public class FlwLibXplatImpl implements FlwLibXplat {
public MultiBlockModelBuilder createMultiBlockModelBuilder(BlockAndTintGetter level, Iterable<BlockPos> positions) {
return new FabricMultiBlockModelBuilder(level, positions);
}
@Override
@Nullable
public ShadersModHandler.InternalHandler createIrisHandler() {
if (!FabricLoader.getInstance()
.isModLoaded("iris")) {
return null;
}
return new ShadersModHandler.InternalHandler() {
@Override
public boolean isShaderPackInUse() {
return IrisApi.getInstance()
.isShaderPackInUse();
}
@Override
public boolean isRenderingShadowPass() {
return IrisApi.getInstance()
.isRenderingShadowPass();
}
};
}
}

View File

@ -0,0 +1,48 @@
package dev.engine_room.flywheel.impl.compat;
import java.util.Optional;
import dev.engine_room.flywheel.impl.FlwImpl;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.VersionParsingException;
import net.fabricmc.loader.api.metadata.version.VersionPredicate;
public final class FabricSodiumCompat {
public static final boolean USE_0_5_COMPAT;
public static final boolean USE_0_6_COMPAT;
static {
boolean use0_5Compat = false;
boolean use0_6Compat = false;
Optional<ModContainer> optionalModContainer = FabricLoader.getInstance().getModContainer(CompatMod.SODIUM.id);
if (optionalModContainer.isPresent()) {
ModContainer modContainer = optionalModContainer.get();
Version sodiumVersion = modContainer.getMetadata().getVersion();
try {
VersionPredicate predicate0_5 = VersionPredicate.parse("~0.5.0");
VersionPredicate predicate0_6 = VersionPredicate.parse(">=0.6.0-beta.2");
use0_5Compat = predicate0_5.test(sodiumVersion);
use0_6Compat = predicate0_6.test(sodiumVersion);
} catch (VersionParsingException e) {
FlwImpl.LOGGER.debug("Failed to parse Sodium version predicates", e);
}
}
USE_0_5_COMPAT = use0_5Compat;
USE_0_6_COMPAT = use0_6Compat;
}
static {
if (USE_0_5_COMPAT) {
FlwImpl.LOGGER.debug("Detected Sodium 0.5");
}
}
private FabricSodiumCompat() {
}
}

View File

@ -1,8 +1,11 @@
package dev.engine_room.flywheel.impl.mixin.sodium;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import dev.engine_room.flywheel.lib.visualization.VisualizationHelper;
import me.jellysquid.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask;
@ -12,11 +15,19 @@ import net.minecraft.world.level.block.entity.BlockEntity;
@Mixin(value = ChunkBuilderMeshingTask.class, remap = false)
abstract class ChunkBuilderMeshingTaskMixin {
@Redirect(method = "execute", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;getRenderer(Lnet/minecraft/world/level/block/entity/BlockEntity;)Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderer;", remap = true))
private BlockEntityRenderer<?> flywheel$redirectGetRenderer(BlockEntityRenderDispatcher dispatcher, BlockEntity blockEntity) {
@WrapOperation(
method = "execute(Lme/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildContext;Lme/jellysquid/mods/sodium/client/util/task/CancellationToken;)Lme/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildOutput;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;getRenderer(Lnet/minecraft/world/level/block/entity/BlockEntity;)Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderer;",
remap = true
)
)
@Nullable
private BlockEntityRenderer<?> flywheel$wrapGetRenderer(BlockEntityRenderDispatcher instance, BlockEntity blockEntity, Operation<BlockEntityRenderer<BlockEntity>> original) {
if (VisualizationHelper.tryAddBlockEntity(blockEntity)) {
return null;
}
return dispatcher.getRenderer(blockEntity);
return original.call(instance, blockEntity);
}
}

View File

@ -2,31 +2,28 @@ package dev.engine_room.flywheel.impl.mixin.sodium;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import com.google.common.base.Suppliers;
import net.fabricmc.loader.api.FabricLoader;
import dev.engine_room.flywheel.impl.compat.FabricSodiumCompat;
public class SodiumMixinPlugin implements IMixinConfigPlugin {
private static final Supplier<Boolean> IS_SODIUM_LOADED = Suppliers.memoize(() -> FabricLoader.getInstance().isModLoaded("sodium"));
@Override
public void onLoad(String mixinPackage) {
}
@Override
@Nullable
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return IS_SODIUM_LOADED.get();
return FabricSodiumCompat.USE_0_5_COMPAT;
}
@Override
@ -34,6 +31,7 @@ public class SodiumMixinPlugin implements IMixinConfigPlugin {
}
@Override
@Nullable
public List<String> getMixins() {
return null;
}

View File

@ -24,14 +24,16 @@
"mixins": [
"flywheel.backend.mixins.json",
"flywheel.impl.mixins.json",
"flywheel.impl.sodium.mixins.json",
"flywheel.impl.fabric.mixins.json"
"flywheel.impl.fabric.mixins.json",
"flywheel.impl.sodium.mixins.json"
],
"depends": {
"minecraft": "${minecraft_semver_version_range}",
"fabricloader": ">=0.15.0",
"fabric-api": "${fabric_api_version_range}"
},
"breaks": {
"sodium": "<0.5.0"
"sodium": ["<0.5.0", "~0.6.0- <0.6.0-beta.2"],
"embeddium": "*"
}
}

View File

@ -10,6 +10,7 @@ plugins {
val api = sourceSets.create("api")
val lib = sourceSets.create("lib")
val backend = sourceSets.create("backend")
val stubs = sourceSets.create("stubs")
val main = sourceSets.getByName("main")
transitiveSourceSets {
@ -26,8 +27,11 @@ transitiveSourceSets {
rootCompile()
compile(api, lib)
}
sourceSet(stubs) {
rootCompile()
}
sourceSet(main) {
compile(api, lib, backend)
compile(api, lib, backend, stubs)
}
createCompileConfigurations()
@ -35,11 +39,10 @@ transitiveSourceSets {
platform {
commonProject = project(":common")
sources(api, lib, backend, main)
compileWithCommonSourceSets()
setupLoomMod()
compileWithCommonSourceSets(api, lib, backend, stubs, main)
setupLoomMod(api, lib, backend, main)
setupLoomRuns()
setupFatJar()
setupFatJar(api, lib, backend, main)
}
jarSets {
@ -70,7 +73,6 @@ loom {
forge {
mixinConfig("flywheel.backend.mixins.json")
mixinConfig("flywheel.impl.mixins.json")
mixinConfig("flywheel.impl.sodium.mixins.json")
}
runs {
@ -85,10 +87,10 @@ dependencies {
forge("net.minecraftforge:forge:${property("minecraft_version")}-${property("forge_version")}")
modCompileOnly("maven.modrinth:embeddium:${property("embeddium_version")}")
modCompileOnly("maven.modrinth:oculus:${property("oculus_version")}")
"forApi"(project(path = ":common", configuration = "commonApiOnly"))
"forLib"(project(path = ":common", configuration = "commonLib"))
"forBackend"(project(path = ":common", configuration = "commonBackend"))
"forStubs"(project(path = ":common", configuration = "commonStubs"))
"forMain"(project(path = ":common", configuration = "commonImpl"))
}

View File

@ -1,10 +1,17 @@
package dev.engine_room.flywheel.impl;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererEvent;
import dev.engine_room.flywheel.impl.compat.CompatMod;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.loading.LoadingModList;
public class FlwImplXplatImpl implements FlwImplXplat {
@Override
public boolean isModLoaded(String modId) {
return LoadingModList.get().getModFileById(modId) != null;
}
@Override
public void dispatchReloadLevelRendererEvent(ClientLevel level) {
MinecraftForge.EVENT_BUS.post(new ReloadLevelRendererEvent(level));
@ -19,4 +26,14 @@ public class FlwImplXplatImpl implements FlwImplXplat {
public FlwConfig getConfig() {
return ForgeFlwConfig.INSTANCE;
}
@Override
public boolean useSodium0_6Compat() {
return CompatMod.SODIUM.isLoaded && !CompatMod.EMBEDDIUM.isLoaded;
}
@Override
public boolean useIrisCompat() {
return CompatMod.IRIS.isLoaded || CompatMod.OCULUS.isLoaded;
}
}

View File

@ -2,7 +2,6 @@ package dev.engine_room.flywheel.impl;
import java.lang.reflect.Field;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import dev.engine_room.flywheel.lib.internal.FlwLibXplat;
@ -12,8 +11,6 @@ import dev.engine_room.flywheel.lib.model.baked.ForgeBakedModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.ForgeBlockModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.ForgeMultiBlockModelBuilder;
import dev.engine_room.flywheel.lib.model.baked.MultiBlockModelBuilder;
import dev.engine_room.flywheel.lib.util.ShadersModHandler;
import net.irisshaders.iris.api.v0.IrisApi;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.client.renderer.block.ModelBlockRenderer;
@ -23,7 +20,6 @@ import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
public class FlwLibXplatImpl implements FlwLibXplat {
@ -64,27 +60,4 @@ public class FlwLibXplatImpl implements FlwLibXplat {
public MultiBlockModelBuilder createMultiBlockModelBuilder(BlockAndTintGetter level, Iterable<BlockPos> positions) {
return new ForgeMultiBlockModelBuilder(level, positions);
}
@Override
@Nullable
public ShadersModHandler.InternalHandler createIrisHandler() {
if (!ModList.get()
.isLoaded("oculus")) {
return null;
}
return new ShadersModHandler.InternalHandler() {
@Override
public boolean isShaderPackInUse() {
return IrisApi.getInstance()
.isShaderPackInUse();
}
@Override
public boolean isRenderingShadowPass() {
return IrisApi.getInstance()
.isRenderingShadowPass();
}
};
}
}

View File

@ -8,6 +8,7 @@ import dev.engine_room.flywheel.api.event.EndClientResourceReloadEvent;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererEvent;
import dev.engine_room.flywheel.backend.compile.FlwProgramsReloader;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.impl.compat.EmbeddiumCompat;
import dev.engine_room.flywheel.impl.visualization.VisualizationEventHandler;
import dev.engine_room.flywheel.lib.model.baked.PartialModelEventHandler;
import dev.engine_room.flywheel.lib.util.LevelAttached;
@ -63,6 +64,8 @@ public final class FlywheelForge {
CrashReportCallables.registerCrashCallable("Flywheel Backend", BackendManagerImpl::getBackendString);
FlwImpl.init();
EmbeddiumCompat.init();
}
private static void registerImplEventListeners(IEventBus forgeEventBus, IEventBus modEventBus) {

View File

@ -0,0 +1,33 @@
package dev.engine_room.flywheel.impl.compat;
import org.embeddedt.embeddium.api.ChunkDataBuiltEvent;
import dev.engine_room.flywheel.impl.FlwImpl;
import dev.engine_room.flywheel.lib.visualization.VisualizationHelper;
public final class EmbeddiumCompat {
public static final boolean ACTIVE = CompatMod.EMBEDDIUM.isLoaded;
static {
if (ACTIVE) {
FlwImpl.LOGGER.debug("Detected Embeddium");
}
}
private EmbeddiumCompat() {
}
public static void init() {
if (ACTIVE) {
Internals.init();
}
}
private static final class Internals {
static void init() {
ChunkDataBuiltEvent.BUS.addListener(event -> {
event.getDataBuilder().removeBlockEntitiesIf(VisualizationHelper::tryAddBlockEntity);
});
}
}
}

View File

@ -1,22 +0,0 @@
package dev.engine_room.flywheel.impl.mixin.sodium;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.engine_room.flywheel.lib.visualization.VisualizationHelper;
import me.jellysquid.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.world.level.block.entity.BlockEntity;
@Mixin(value = ChunkBuilderMeshingTask.class, remap = false)
abstract class ChunkBuilderMeshingTaskMixin {
@Redirect(method = "execute", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;getRenderer(Lnet/minecraft/world/level/block/entity/BlockEntity;)Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderer;", remap = true))
private BlockEntityRenderer<?> flywheel$redirectGetRenderer(BlockEntityRenderDispatcher dispatcher, BlockEntity blockEntity) {
if (VisualizationHelper.tryAddBlockEntity(blockEntity)) {
return null;
}
return dispatcher.getRenderer(blockEntity);
}
}

View File

@ -1,48 +0,0 @@
package dev.engine_room.flywheel.impl.mixin.sodium;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import com.google.common.base.Suppliers;
import net.minecraftforge.fml.loading.LoadingModList;
public class SodiumMixinPlugin implements IMixinConfigPlugin {
private static final Supplier<Boolean> IS_SODIUM_LOADED = Suppliers.memoize(() -> LoadingModList.get().getModFileById("rubidium") != null);
@Override
public void onLoad(String mixinPackage) {
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return IS_SODIUM_LOADED.get();
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> getMixins() {
return null;
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}

View File

@ -26,10 +26,21 @@ mandatory = true
versionRange = "${forge_version_range}"
side = "CLIENT"
# Simulates a breaks/incompatible dependency
[[dependencies.${mod_id}]]
# This replicates a "breaks" dependency.
# There's a mixin crash with Rubidium <0.7.0.
modId = "rubidium"
mandatory = false
versionRange = "[0.7.0,)"
versionRange = "[0.0-INCOMPATIBLE]"
side = "CLIENT"
[[dependencies.${mod_id}]]
modId = "embeddium"
mandatory = false
versionRange = "[0.3.25,)"
side = "CLIENT"
[[dependencies.${mod_id}]]
modId = "sodium"
mandatory = false
versionRange = "[0.6.0-beta.2,)"
side = "CLIENT"

View File

@ -1,14 +0,0 @@
{
"required": true,
"minVersion": "0.8",
"package": "dev.engine_room.flywheel.impl.mixin.sodium",
"compatibilityLevel": "JAVA_17",
"refmap": "flywheel.refmap.json",
"plugin": "dev.engine_room.flywheel.impl.mixin.sodium.SodiumMixinPlugin",
"client": [
"ChunkBuilderMeshingTaskMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@ -7,9 +7,9 @@ mod_name = Flywheel
mod_version = 1.0.0-beta
mod_description = An overhauled entity and block entity rendering API.
mod_license = MIT
mod_sources = https://github.com/Jozufozu/Flywheel
mod_issues = https://github.com/Jozufozu/Flywheel/issues
mod_homepage = https://github.com/Jozufozu/Flywheel
mod_sources = https://github.com/Engine-Room/Flywheel
mod_issues = https://github.com/Engine-Room/Flywheel/issues
mod_homepage = https://github.com/Engine-Room/Flywheel
# Mod dependency declarations
minecraft_semver_version_range = >=1.20.1 <1.20.2
@ -21,6 +21,7 @@ forge_version_range = [47.0.0,)
java_version = 17
arch_loom_version = 1.7.412
cursegradle_version = 1.4.0
parchment_minecraft_version = 1.20.1
parchment_version = 2023.09.03
# Minecraft build dependency versions
@ -30,10 +31,8 @@ fabric_loader_version = 0.15.9
fabric_api_version = 0.92.1+1.20.1
# Build dependency mod versions
sodium_version = mc1.20.1-0.5.8
iris_version = 1.6.17+1.20.1
embeddium_version = 0.3.9+mc1.20.1
oculus_version = 1.20.1-1.6.15a
sodium_version = mc1.20.1-0.5.11
embeddium_version = 0.3.25+mc1.20.1
# Publication info
group = dev.engine_room.flywheel

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

7
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# 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
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################