Store bought engines

- Add InstancerStorage so engines can share common code.
- Instanced and Indirect DrawManagers extend InstancerStorage, while
  BatchingEngine keeps an anonymous class for it.
- AbstractEngine now has a InstancerStorage getter and does some
  delegation so the implementations don't have to.
- InstancedInstancer directly stores the list of DrawCalls it belongs
  to.
- InstancingEngine no longer accepts a context parameter.
- Make the /flywheel backend command default to the flywheel nampspace.
This commit is contained in:
Jozufozu 2023-11-26 12:07:05 -08:00
parent 8fa095cd16
commit 2039a964e2
11 changed files with 274 additions and 247 deletions

View file

@ -10,7 +10,6 @@ import com.jozufozu.flywheel.backend.engine.indirect.IndirectEngine;
import com.jozufozu.flywheel.backend.engine.instancing.InstancingEngine; import com.jozufozu.flywheel.backend.engine.instancing.InstancingEngine;
import com.jozufozu.flywheel.gl.GlCompat; import com.jozufozu.flywheel.gl.GlCompat;
import com.jozufozu.flywheel.lib.backend.SimpleBackend; import com.jozufozu.flywheel.lib.backend.SimpleBackend;
import com.jozufozu.flywheel.lib.context.Contexts;
import com.jozufozu.flywheel.lib.util.ShadersModHandler; import com.jozufozu.flywheel.lib.util.ShadersModHandler;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
@ -33,7 +32,7 @@ public class Backends {
public static final Backend INSTANCING = SimpleBackend.builder() public static final Backend INSTANCING = SimpleBackend.builder()
.engineMessage(Component.literal("Using Instancing Engine") .engineMessage(Component.literal("Using Instancing Engine")
.withStyle(ChatFormatting.GREEN)) .withStyle(ChatFormatting.GREEN))
.engineFactory(level -> new InstancingEngine(256, Contexts.WORLD)) .engineFactory(level -> new InstancingEngine(256))
.fallback(() -> Backends.BATCHING) .fallback(() -> Backends.BATCHING)
.supported(() -> !ShadersModHandler.isShaderPackInUse() && GlCompat.supportsInstancing() && InstancingPrograms.allLoaded()) .supported(() -> !ShadersModHandler.isShaderPackInUse() && GlCompat.supportsInstancing() && InstancingPrograms.allLoaded())
.register(Flywheel.rl("instancing")); .register(Flywheel.rl("instancing"));

View file

@ -1,6 +1,11 @@
package com.jozufozu.flywheel.backend.engine; package com.jozufozu.flywheel.backend.engine;
import com.jozufozu.flywheel.api.backend.Engine; import com.jozufozu.flywheel.api.backend.Engine;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
import net.minecraft.client.Camera; import net.minecraft.client.Camera;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
@ -15,6 +20,11 @@ public abstract class AbstractEngine implements Engine {
sqrMaxOriginDistance = maxOriginDistance * maxOriginDistance; sqrMaxOriginDistance = maxOriginDistance * maxOriginDistance;
} }
@Override
public <I extends Instance> Instancer<I> instancer(InstanceType<I> type, Model model, RenderStage stage) {
return getStorage().getInstancer(type, model, stage);
}
@Override @Override
public boolean updateRenderOrigin(Camera camera) { public boolean updateRenderOrigin(Camera camera) {
Vec3 cameraPos = camera.getPosition(); Vec3 cameraPos = camera.getPosition();
@ -28,7 +38,7 @@ public abstract class AbstractEngine implements Engine {
} }
renderOrigin = BlockPos.containing(cameraPos); renderOrigin = BlockPos.containing(cameraPos);
onRenderOriginChanged(); getStorage().onRenderOriginChanged();
return true; return true;
} }
@ -37,6 +47,5 @@ public abstract class AbstractEngine implements Engine {
return renderOrigin; return renderOrigin;
} }
protected void onRenderOriginChanged() { protected abstract InstancerStorage<? extends AbstractInstancer<?>> getStorage();
}
} }

View file

@ -108,4 +108,8 @@ public abstract class AbstractInstancer<I extends Instance> implements Instancer
public String toString() { public String toString() {
return "AbstractInstancer[" + getInstanceCount() + ']'; return "AbstractInstancer[" + getInstanceCount() + ']';
} }
public void delete() {
}
} }

View file

@ -0,0 +1,96 @@
package com.jozufozu.flywheel.backend.engine;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
public abstract class InstancerStorage<N extends AbstractInstancer<?>> {
/**
* A map of instancer keys to instancers.
* <br>
* This map is populated as instancers are requested and contains both initialized and uninitialized instancers.
* Write access to this map must be synchronized on {@link #creationLock}.
* <br>
* See {@link #getInstancer} for insertion details.
*/
private final Map<InstancerKey<?>, N> instancers = new HashMap<>();
/**
* A list of instancers that have not yet been initialized.
* <br>
* All new instancers land here before having resources allocated in {@link #flush}.
* Write access to this list must be synchronized on {@link #creationLock}.
*/
private final List<UninitializedInstancer<N, ?>> uninitializedInstancers = new ArrayList<>();
/**
* Mutex for {@link #instancers} and {@link #uninitializedInstancers}.
*/
private final Object creationLock = new Object();
/**
* A list of initialized instancers.
* <br>
* These are instancers that may need to be cleared or deleted.
*/
private final List<N> initializedInstancers = new ArrayList<>();
@SuppressWarnings("unchecked")
public <I extends Instance> Instancer<I> getInstancer(InstanceType<I> type, Model model, RenderStage stage) {
InstancerKey<I> key = new InstancerKey<>(type, model, stage);
N instancer = instancers.get(key);
// Happy path: instancer is already initialized.
if (instancer != null) {
return (Instancer<I>) instancer;
}
// Unhappy path: instancer is not initialized, need to sync to make sure we don't create duplicates.
synchronized (creationLock) {
// Someone else might have initialized it while we were waiting for the lock.
instancer = instancers.get(key);
if (instancer != null) {
return (Instancer<I>) instancer;
}
// Create a new instancer and add it to the uninitialized list.
instancer = create(type);
instancers.put(key, instancer);
uninitializedInstancers.add(new UninitializedInstancer<>(key, instancer, model, stage));
return (Instancer<I>) instancer;
}
}
public void invalidate() {
instancers.clear();
uninitializedInstancers.clear();
initializedInstancers.forEach(AbstractInstancer::delete);
initializedInstancers.clear();
}
public void flush() {
for (var instancer : uninitializedInstancers) {
add(instancer.key(), instancer.instancer(), instancer.model(), instancer.stage());
initializedInstancers.add(instancer.instancer());
}
uninitializedInstancers.clear();
}
public void onRenderOriginChanged() {
initializedInstancers.forEach(AbstractInstancer::clear);
}
protected abstract <I extends Instance> N create(InstanceType<I> type);
protected abstract <I extends Instance> void add(InstancerKey<I> key, N instancer, Model model, RenderStage stage);
private record UninitializedInstancer<N, I extends Instance>(InstancerKey<I> key, N instancer, Model model, RenderStage stage) {
}
}

View file

@ -1,6 +1,5 @@
package com.jozufozu.flywheel.backend.engine.batching; package com.jozufozu.flywheel.backend.engine.batching;
import java.util.ArrayList;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -10,13 +9,14 @@ import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance; import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType; import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Mesh; import com.jozufozu.flywheel.api.model.Mesh;
import com.jozufozu.flywheel.api.model.Model; import com.jozufozu.flywheel.api.model.Model;
import com.jozufozu.flywheel.api.task.Plan; import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor; import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.engine.AbstractEngine; import com.jozufozu.flywheel.backend.engine.AbstractEngine;
import com.jozufozu.flywheel.backend.engine.AbstractInstancer;
import com.jozufozu.flywheel.backend.engine.InstancerKey; import com.jozufozu.flywheel.backend.engine.InstancerKey;
import com.jozufozu.flywheel.backend.engine.InstancerStorage;
import com.jozufozu.flywheel.lib.task.Flag; import com.jozufozu.flywheel.lib.task.Flag;
import com.jozufozu.flywheel.lib.task.NamedFlag; import com.jozufozu.flywheel.lib.task.NamedFlag;
import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; import com.jozufozu.flywheel.lib.task.SimplyComposedPlan;
@ -27,9 +27,27 @@ import net.minecraft.client.renderer.RenderType;
public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan<RenderContext> { public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan<RenderContext> {
private final BatchedDrawTracker drawTracker = new BatchedDrawTracker(); private final BatchedDrawTracker drawTracker = new BatchedDrawTracker();
private final Map<InstancerKey<?>, BatchedInstancer<?>> instancers = new HashMap<>();
private final List<UninitializedInstancer> uninitializedInstancers = new ArrayList<>(); // TODO: reintroduce BatchedDrawManager
private final List<BatchedInstancer<?>> initializedInstancers = new ArrayList<>(); private final InstancerStorage<BatchedInstancer<?>> storage = new InstancerStorage<>() {
@Override
protected <I extends Instance> BatchedInstancer<?> create(InstanceType<I> type) {
return new BatchedInstancer<>(type);
}
@Override
protected <I extends Instance> void add(InstancerKey<I> key, BatchedInstancer<?> instancer, Model model, RenderStage stage) {
var stagePlan = stagePlans.computeIfAbsent(stage, renderStage -> new BatchedStagePlan(renderStage, drawTracker));
var meshes = model.getMeshes();
for (var entry : meshes.entrySet()) {
var material = entry.getKey();
RenderType renderType = material.getFallbackRenderType();
var transformCall = new TransformCall<>(instancer, material, alloc(entry.getValue(), renderType.format()));
stagePlan.put(renderType, transformCall);
}
}
};
private final Map<RenderStage, BatchedStagePlan> stagePlans = new EnumMap<>(RenderStage.class); private final Map<RenderStage, BatchedStagePlan> stagePlans = new EnumMap<>(RenderStage.class);
private final Map<VertexFormat, BatchedMeshPool> meshPools = new HashMap<>(); private final Map<VertexFormat, BatchedMeshPool> meshPools = new HashMap<>();
@ -39,20 +57,6 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
super(maxOriginDistance); super(maxOriginDistance);
} }
@Override
@SuppressWarnings("unchecked")
public <I extends Instance> Instancer<I> instancer(InstanceType<I> type, Model model, RenderStage stage) {
InstancerKey<I> key = new InstancerKey<>(type, model, stage);
BatchedInstancer<I> instancer = (BatchedInstancer<I>) instancers.get(key);
// FIXME: This needs to be synchronized like InstancingEngine
if (instancer == null) {
instancer = new BatchedInstancer<>(type);
instancers.put(key, instancer);
uninitializedInstancers.add(new UninitializedInstancer(instancer, model, stage));
}
return instancer;
}
@Override @Override
public void execute(TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) { public void execute(TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) {
flush(); flush();
@ -100,49 +104,29 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
} }
@Override @Override
protected void onRenderOriginChanged() { protected InstancerStorage<? extends AbstractInstancer<?>> getStorage() {
initializedInstancers.forEach(BatchedInstancer::clear); return storage;
} }
@Override @Override
public void delete() { public void delete() {
instancers.clear(); storage.invalidate();
meshPools.values() meshPools.values()
.forEach(BatchedMeshPool::delete); .forEach(BatchedMeshPool::delete);
meshPools.clear(); meshPools.clear();
initializedInstancers.clear();
} }
private void flush() { private void flush() {
for (var instancer : uninitializedInstancers) { storage.flush();
add(instancer.instancer(), instancer.model(), instancer.stage());
}
uninitializedInstancers.clear();
for (var pool : meshPools.values()) { for (var pool : meshPools.values()) {
pool.flush(); pool.flush();
} }
} }
private void add(BatchedInstancer<?> instancer, Model model, RenderStage stage) {
var stagePlan = stagePlans.computeIfAbsent(stage, renderStage -> new BatchedStagePlan(renderStage, drawTracker));
var meshes = model.getMeshes();
for (var entry : meshes.entrySet()) {
var material = entry.getKey();
RenderType renderType = material.getFallbackRenderType();
var transformCall = new TransformCall<>(instancer, material, alloc(entry.getValue(), renderType.format()));
stagePlan.put(renderType, transformCall);
}
initializedInstancers.add(instancer);
}
private BatchedMeshPool.BufferedMesh alloc(Mesh mesh, VertexFormat format) { private BatchedMeshPool.BufferedMesh alloc(Mesh mesh, VertexFormat format) {
return meshPools.computeIfAbsent(format, BatchedMeshPool::new) return meshPools.computeIfAbsent(format, BatchedMeshPool::new)
.alloc(mesh); .alloc(mesh);
} }
private record UninitializedInstancer(BatchedInstancer<?> instancer, Model model, RenderStage stage) {
}
} }

View file

@ -1,43 +1,22 @@
package com.jozufozu.flywheel.backend.engine.indirect; package com.jozufozu.flywheel.backend.engine.indirect;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance; import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType; import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model; import com.jozufozu.flywheel.api.model.Model;
import com.jozufozu.flywheel.api.vertex.VertexType; import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.engine.InstancerKey; import com.jozufozu.flywheel.backend.engine.InstancerKey;
import com.jozufozu.flywheel.backend.engine.InstancerStorage;
import com.jozufozu.flywheel.lib.util.Pair; import com.jozufozu.flywheel.lib.util.Pair;
public class IndirectDrawManager { public class IndirectDrawManager extends InstancerStorage<IndirectInstancer<?>> {
private final Map<InstancerKey<?>, IndirectInstancer<?>> instancers = new HashMap<>();
private final List<UninitializedInstancer> uninitializedInstancers = new ArrayList<>();
private final List<IndirectInstancer<?>> initializedInstancers = new ArrayList<>();
public final Map<Pair<InstanceType<?>, VertexType>, IndirectCullingGroup<?>> renderLists = new HashMap<>(); public final Map<Pair<InstanceType<?>, VertexType>, IndirectCullingGroup<?>> renderLists = new HashMap<>();
@SuppressWarnings("unchecked")
public <I extends Instance> Instancer<I> getInstancer(InstanceType<I> type, Model model, RenderStage stage) {
InstancerKey<I> key = new InstancerKey<>(type, model, stage);
// FIXME: This needs to be synchronized like InstancingEngine
IndirectInstancer<I> instancer = (IndirectInstancer<I>) instancers.get(key);
if (instancer == null) {
instancer = new IndirectInstancer<>(type);
instancers.put(key, instancer);
uninitializedInstancers.add(new UninitializedInstancer(instancer, model, stage));
}
return instancer;
}
public void flush() { public void flush() {
for (var instancer : uninitializedInstancers) { super.flush();
add(instancer.instancer(), instancer.model(), instancer.stage());
}
uninitializedInstancers.clear();
for (IndirectCullingGroup<?> value : renderLists.values()) { for (IndirectCullingGroup<?> value : renderLists.values()) {
value.beginFrame(); value.beginFrame();
@ -45,33 +24,11 @@ public class IndirectDrawManager {
} }
public void invalidate() { public void invalidate() {
instancers.clear(); super.invalidate();
renderLists.values() renderLists.values()
.forEach(IndirectCullingGroup::delete); .forEach(IndirectCullingGroup::delete);
renderLists.clear(); renderLists.clear();
initializedInstancers.clear();
}
public void clearInstancers() {
initializedInstancers.forEach(IndirectInstancer::clear);
}
@SuppressWarnings("unchecked")
private <I extends Instance> void add(IndirectInstancer<I> instancer, Model model, RenderStage stage) {
var meshes = model.getMeshes();
for (var entry : meshes.entrySet()) {
var material = entry.getKey();
var mesh = entry.getValue();
var indirectList = (IndirectCullingGroup<I>) renderLists.computeIfAbsent(Pair.of(instancer.type, mesh.vertexType()), p -> new IndirectCullingGroup<>(p.first(), p.second()));
indirectList.add(instancer, stage, material, mesh);
break; // TODO: support multiple meshes per model
}
initializedInstancers.add(instancer);
} }
public boolean hasStage(RenderStage stage) { public boolean hasStage(RenderStage stage) {
@ -83,6 +40,23 @@ public class IndirectDrawManager {
return false; return false;
} }
private record UninitializedInstancer(IndirectInstancer<?> instancer, Model model, RenderStage stage) { @Override
protected <I extends Instance> IndirectInstancer<?> create(InstanceType<I> type) {
return new IndirectInstancer<>(type);
}
@Override
protected <I extends Instance> void add(InstancerKey<I> key, IndirectInstancer<?> instancer, Model model, RenderStage stage) {
var meshes = model.getMeshes();
for (var entry : meshes.entrySet()) {
var material = entry.getKey();
var mesh = entry.getValue();
var indirectList = (IndirectCullingGroup<I>) renderLists.computeIfAbsent(Pair.of(key.type(), mesh.vertexType()), p -> new IndirectCullingGroup<>(p.first(), p.second()));
indirectList.add((IndirectInstancer<I>) instancer, stage, material, mesh);
break; // TODO: support multiple meshes per model
}
} }
} }

View file

@ -7,17 +7,15 @@ import org.lwjgl.opengl.GL32;
import com.jozufozu.flywheel.api.event.RenderContext; import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance; import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
import com.jozufozu.flywheel.api.task.Plan; import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor; import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.engine.AbstractEngine; import com.jozufozu.flywheel.backend.engine.AbstractEngine;
import com.jozufozu.flywheel.backend.engine.AbstractInstancer;
import com.jozufozu.flywheel.backend.engine.InstancerStorage;
import com.jozufozu.flywheel.gl.GlStateTracker; import com.jozufozu.flywheel.gl.GlStateTracker;
import com.jozufozu.flywheel.gl.GlTextureUnit; import com.jozufozu.flywheel.gl.GlTextureUnit;
import com.jozufozu.flywheel.lib.task.Flag; import com.jozufozu.flywheel.lib.task.Flag;
import com.jozufozu.flywheel.lib.task.NamedFlag; import com.jozufozu.flywheel.lib.task.NamedFlag;
import com.jozufozu.flywheel.lib.task.RaisePlan;
import com.jozufozu.flywheel.lib.task.SyncedPlan; import com.jozufozu.flywheel.lib.task.SyncedPlan;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
@ -31,44 +29,37 @@ public class IndirectEngine extends AbstractEngine {
super(maxOriginDistance); super(maxOriginDistance);
} }
@Override
public <I extends Instance> Instancer<I> instancer(InstanceType<I> type, Model model, RenderStage stage) {
return drawManager.getInstancer(type, model, stage);
}
@Override @Override
public Plan<RenderContext> createFramePlan() { public Plan<RenderContext> createFramePlan() {
return SyncedPlan.<RenderContext>of(this::flushDrawManager) return SyncedPlan.of(this::flushDrawManager);
.then(RaisePlan.raise(flushFlag));
} }
private void flushDrawManager() { private void flushDrawManager() {
try (var state = GlStateTracker.getRestoreState()) { try (var state = GlStateTracker.getRestoreState()) {
drawManager.flush(); drawManager.flush();
} }
flushFlag.raise();
} }
@Override @Override
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) { public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
if (drawManager.hasStage(stage)) { executor.syncUntil(flushFlag::isRaised);
executor.syncUntil(flushFlag::isRaised);
try (var restoreState = GlStateTracker.getRestoreState()) {
setup();
for (var list : drawManager.renderLists.values()) {
list.submit(stage);
}
}
}
if (stage.isLast()) { if (stage.isLast()) {
// Need to sync here to ensure this frame has everything executed
// in case we didn't have any stages to draw this frame.
executor.syncUntil(flushFlag::isRaised);
flushFlag.lower(); flushFlag.lower();
} }
}
if (!drawManager.hasStage(stage)) {
return;
}
try (var restoreState = GlStateTracker.getRestoreState()) {
setup();
for (var list : drawManager.renderLists.values()) {
list.submit(stage);
}
}
}
@Override @Override
public void renderCrumblingInstances(TaskExecutor executor, RenderContext context, List<Instance> instances, int progress) { public void renderCrumblingInstances(TaskExecutor executor, RenderContext context, List<Instance> instances, int progress) {
@ -87,8 +78,8 @@ public class IndirectEngine extends AbstractEngine {
} }
@Override @Override
protected void onRenderOriginChanged() { protected InstancerStorage<? extends AbstractInstancer<?>> getStorage() {
drawManager.clearInstancers(); return drawManager;
} }
@Override @Override

View file

@ -1,11 +1,9 @@
package com.jozufozu.flywheel.backend.engine.instancing; package com.jozufozu.flywheel.backend.engine.instancing;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -16,40 +14,14 @@ import com.google.common.collect.ListMultimap;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance; import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType; import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Mesh; import com.jozufozu.flywheel.api.model.Mesh;
import com.jozufozu.flywheel.api.model.Model; import com.jozufozu.flywheel.api.model.Model;
import com.jozufozu.flywheel.api.vertex.VertexType; import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.engine.InstancerKey; import com.jozufozu.flywheel.backend.engine.InstancerKey;
import com.jozufozu.flywheel.backend.engine.InstancerStorage;
public class InstancedDrawManager { public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>> {
/**
* A map of instancer keys to instancers.
* <br>
* This map is populated as instancers are requested and contains both initialized and uninitialized instancers.
* Write access to this map must be synchronized on {@link #creationLock}.
* <br>
* See {@link #getInstancer} for insertion details.
*/
private final Map<InstancerKey<?>, InstancedInstancer<?>> instancers = new HashMap<>();
/**
* A list of instancers that have not yet been initialized.
* <br>
* All new instancers land here before having resources allocated in {@link #flush}.
* Write access to this list must be synchronized on {@link #creationLock}.
*/
private final List<UninitializedInstancer> uninitializedInstancers = new ArrayList<>();
/**
* Mutex for {@link #instancers} and {@link #uninitializedInstancers}.
*/
private final Object creationLock = new Object();
/**
* A map of initialized instancers to their draw calls.
* <br>
* This map is populated in {@link #flush} and contains only initialized instancers.
*/
private final Map<InstancedInstancer<?>, List<DrawCall>> initializedInstancers = new HashMap<>();
/** /**
* The set of draw calls to make in each {@link RenderStage}. * The set of draw calls to make in each {@link RenderStage}.
*/ */
@ -64,37 +36,8 @@ public class InstancedDrawManager {
return drawSets.getOrDefault(stage, DrawSet.EMPTY); return drawSets.getOrDefault(stage, DrawSet.EMPTY);
} }
@SuppressWarnings("unchecked")
public <I extends Instance> Instancer<I> getInstancer(InstanceType<I> type, Model model, RenderStage stage) {
InstancerKey<I> key = new InstancerKey<>(type, model, stage);
InstancedInstancer<I> instancer = (InstancedInstancer<I>) instancers.get(key);
// Happy path: instancer is already initialized.
if (instancer != null) {
return instancer;
}
// Unhappy path: instancer is not initialized, need to sync to make sure we don't create duplicates.
synchronized (creationLock) {
// Someone else might have initialized it while we were waiting for the lock.
instancer = (InstancedInstancer<I>) instancers.get(key);
if (instancer != null) {
return instancer;
}
// Create a new instancer and add it to the uninitialized list.
instancer = new InstancedInstancer<>(type);
instancers.put(key, instancer);
uninitializedInstancers.add(new UninitializedInstancer(instancer, model, stage));
return instancer;
}
}
public void flush() { public void flush() {
for (var instancer : uninitializedInstancers) { super.flush();
add(instancer.instancer(), instancer.model(), instancer.stage());
}
uninitializedInstancers.clear();
for (var pool : meshPools.values()) { for (var pool : meshPools.values()) {
pool.flush(); pool.flush();
@ -102,7 +45,7 @@ public class InstancedDrawManager {
} }
public void invalidate() { public void invalidate() {
instancers.clear(); super.invalidate();
meshPools.values() meshPools.values()
.forEach(InstancedMeshPool::delete); .forEach(InstancedMeshPool::delete);
@ -112,23 +55,24 @@ public class InstancedDrawManager {
.forEach(DrawSet::delete); .forEach(DrawSet::delete);
drawSets.clear(); drawSets.clear();
initializedInstancers.keySet()
.forEach(InstancedInstancer::delete);
initializedInstancers.clear();
eboCache.invalidate(); eboCache.invalidate();
} }
public void clearInstancers() { private InstancedMeshPool.BufferedMesh alloc(Mesh mesh) {
initializedInstancers.keySet() return meshPools.computeIfAbsent(mesh.vertexType(), InstancedMeshPool::new)
.forEach(InstancedInstancer::clear); .alloc(mesh, eboCache);
} }
private void add(InstancedInstancer<?> instancer, Model model, RenderStage stage) { @Override
protected <I extends Instance> InstancedInstancer<I> create(InstanceType<I> type) {
return new InstancedInstancer<>(type);
}
@Override
protected <I extends Instance> void add(InstancerKey<I> key, InstancedInstancer<?> instancer, Model model, RenderStage stage) {
instancer.init(); instancer.init();
DrawSet drawSet = drawSets.computeIfAbsent(stage, DrawSet::new); DrawSet drawSet = drawSets.computeIfAbsent(stage, DrawSet::new);
List<DrawCall> drawCalls = new ArrayList<>();
var meshes = model.getMeshes(); var meshes = model.getMeshes();
for (var entry : meshes.entrySet()) { for (var entry : meshes.entrySet()) {
@ -138,18 +82,8 @@ public class InstancedDrawManager {
DrawCall drawCall = new DrawCall(instancer, mesh, shaderState); DrawCall drawCall = new DrawCall(instancer, mesh, shaderState);
drawSet.put(shaderState, drawCall); drawSet.put(shaderState, drawCall);
drawCalls.add(drawCall); instancer.addDrawCall(drawCall);
} }
initializedInstancers.put(instancer, drawCalls);
}
private InstancedMeshPool.BufferedMesh alloc(Mesh mesh) {
return meshPools.computeIfAbsent(mesh.vertexType(), InstancedMeshPool::new)
.alloc(mesh, eboCache);
}
public List<DrawCall> drawCallsForInstancer(InstancedInstancer<?> instancer) {
return initializedInstancers.getOrDefault(instancer, List.of());
} }
public static class DrawSet implements Iterable<Map.Entry<ShaderState, Collection<DrawCall>>> { public static class DrawSet implements Iterable<Map.Entry<ShaderState, Collection<DrawCall>>> {
@ -187,7 +121,4 @@ public class InstancedDrawManager {
.iterator(); .iterator();
} }
} }
private record UninitializedInstancer(InstancedInstancer<?> instancer, Model model, RenderStage stage) {
}
} }

View file

@ -1,6 +1,8 @@
package com.jozufozu.flywheel.backend.engine.instancing; package com.jozufozu.flywheel.backend.engine.instancing;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.Flywheel;
@ -21,6 +23,8 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
private final Set<GlVertexArray> boundTo = new HashSet<>(); private final Set<GlVertexArray> boundTo = new HashSet<>();
private GlBuffer vbo; private GlBuffer vbo;
private final List<DrawCall> drawCalls = new ArrayList<>();
public InstancedInstancer(InstanceType<I> type) { public InstancedInstancer(InstanceType<I> type) {
super(type); super(type);
instanceFormat = type.getLayout(); instanceFormat = type.getLayout();
@ -111,4 +115,12 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
vbo.delete(); vbo.delete();
vbo = null; vbo = null;
} }
public void addDrawCall(DrawCall drawCall) {
drawCalls.add(drawCall);
}
public List<DrawCall> drawCalls() {
return drawCalls;
}
} }

View file

@ -12,23 +12,20 @@ import com.jozufozu.flywheel.api.context.Context;
import com.jozufozu.flywheel.api.event.RenderContext; import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance; import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
import com.jozufozu.flywheel.api.task.Plan; import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor; import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.compile.InstancingPrograms; import com.jozufozu.flywheel.backend.compile.InstancingPrograms;
import com.jozufozu.flywheel.backend.engine.AbstractEngine; import com.jozufozu.flywheel.backend.engine.AbstractEngine;
import com.jozufozu.flywheel.backend.engine.AbstractInstancer;
import com.jozufozu.flywheel.backend.engine.InstanceHandleImpl; import com.jozufozu.flywheel.backend.engine.InstanceHandleImpl;
import com.jozufozu.flywheel.backend.engine.InstancerStorage;
import com.jozufozu.flywheel.backend.engine.UniformBuffer; import com.jozufozu.flywheel.backend.engine.UniformBuffer;
import com.jozufozu.flywheel.backend.engine.indirect.Textures;
import com.jozufozu.flywheel.gl.GlStateTracker; import com.jozufozu.flywheel.gl.GlStateTracker;
import com.jozufozu.flywheel.gl.GlTextureUnit; import com.jozufozu.flywheel.gl.GlTextureUnit;
import com.jozufozu.flywheel.lib.context.Contexts; import com.jozufozu.flywheel.lib.context.Contexts;
import com.jozufozu.flywheel.lib.material.MaterialIndices; import com.jozufozu.flywheel.lib.material.MaterialIndices;
import com.jozufozu.flywheel.lib.task.Flag; import com.jozufozu.flywheel.lib.task.Flag;
import com.jozufozu.flywheel.lib.task.NamedFlag; import com.jozufozu.flywheel.lib.task.NamedFlag;
import com.jozufozu.flywheel.lib.task.RaisePlan;
import com.jozufozu.flywheel.lib.task.SyncedPlan; import com.jozufozu.flywheel.lib.task.SyncedPlan;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
@ -36,54 +33,45 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.ModelBakery;
public class InstancingEngine extends AbstractEngine { public class InstancingEngine extends AbstractEngine {
private final Context context;
private final InstancedDrawManager drawManager = new InstancedDrawManager(); private final InstancedDrawManager drawManager = new InstancedDrawManager();
private final Flag flushFlag = new NamedFlag("flushed"); private final Flag flushFlag = new NamedFlag("flushed");
public InstancingEngine(int maxOriginDistance, Context context) { public InstancingEngine(int maxOriginDistance) {
super(maxOriginDistance); super(maxOriginDistance);
this.context = context; }
}
@Override
public <I extends Instance> Instancer<I> instancer(InstanceType<I> type, Model model, RenderStage stage) {
return drawManager.getInstancer(type, model, stage);
}
@Override @Override
public Plan<RenderContext> createFramePlan() { public Plan<RenderContext> createFramePlan() {
return SyncedPlan.<RenderContext>of(this::flushDrawManager) return SyncedPlan.of(this::flushDrawManager);
.then(RaisePlan.raise(flushFlag));
} }
private void flushDrawManager() { private void flushDrawManager() {
try (var restoreState = GlStateTracker.getRestoreState()) { try (var restoreState = GlStateTracker.getRestoreState()) {
drawManager.flush(); drawManager.flush();
} }
flushFlag.raise();
} }
@Override @Override
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) { public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
var drawSet = drawManager.get(stage); executor.syncUntil(flushFlag::isRaised);
if (!drawSet.isEmpty()) {
executor.syncUntil(flushFlag::isRaised);
try (var state = GlStateTracker.getRestoreState()) {
setup();
render(drawSet);
}
}
if (stage.isLast()) { if (stage.isLast()) {
// Need to sync here to ensure this frame has everything executed
// in case we didn't have any stages to draw this frame.
executor.syncUntil(flushFlag::isRaised);
flushFlag.lower(); flushFlag.lower();
} }
}
var drawSet = drawManager.get(stage);
if (drawSet.isEmpty()) {
return;
}
try (var state = GlStateTracker.getRestoreState()) {
setup();
render(drawSet);
}
}
@Override @Override
public void renderCrumblingInstances(TaskExecutor executor, RenderContext context, List<Instance> instances, int progress) { public void renderCrumblingInstances(TaskExecutor executor, RenderContext context, List<Instance> instances, int progress) {
@ -139,7 +127,7 @@ public class InstancingEngine extends AbstractEngine {
continue; continue;
} }
List<DrawCall> draws = drawManager.drawCallsForInstancer(instancer); List<DrawCall> draws = instancer.drawCalls();
draws.removeIf(DrawCall::isInvalid); draws.removeIf(DrawCall::isInvalid);
@ -173,7 +161,7 @@ public class InstancingEngine extends AbstractEngine {
continue; continue;
} }
setup(shader, context); setup(shader, Contexts.WORLD);
shader.material().setup(); shader.material().setup();
@ -201,8 +189,8 @@ public class InstancingEngine extends AbstractEngine {
} }
@Override @Override
protected void onRenderOriginChanged() { protected InstancerStorage<? extends AbstractInstancer<?>> getStorage() {
drawManager.clearInstancers(); return drawManager;
} }
@Override @Override

View file

@ -4,21 +4,39 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.jetbrains.annotations.NotNull;
import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.api.backend.Backend; import com.jozufozu.flywheel.api.backend.Backend;
import com.jozufozu.flywheel.lib.util.ResourceUtil;
import com.mojang.brigadier.StringReader; import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.ResourceLocationException;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
public class BackendArgument implements ArgumentType<Backend> { public class BackendArgument implements ArgumentType<Backend> {
private static final List<String> STRING_IDS = Backend.REGISTRY.getAllIds().stream().map(ResourceLocation::toString).toList(); private static final List<String> STRING_IDS = Backend.REGISTRY.getAllIds()
.stream()
.map(rl -> {
if (Flywheel.ID
.equals(rl.getNamespace())) {
return rl.getPath();
} else {
return rl.toString();
}
})
.toList();
private static final SimpleCommandExceptionType ERROR_INVALID = new SimpleCommandExceptionType(Component.translatable("argument.id.invalid"));
public static final DynamicCommandExceptionType ERROR_UNKNOWN_BACKEND = new DynamicCommandExceptionType(arg -> { public static final DynamicCommandExceptionType ERROR_UNKNOWN_BACKEND = new DynamicCommandExceptionType(arg -> {
return Component.literal("Unknown backend '" + arg + "'"); return Component.literal("Unknown backend '" + arg + "'");
@ -28,7 +46,7 @@ public class BackendArgument implements ArgumentType<Backend> {
@Override @Override
public Backend parse(StringReader reader) throws CommandSyntaxException { public Backend parse(StringReader reader) throws CommandSyntaxException {
ResourceLocation id = ResourceLocation.read(reader); ResourceLocation id = getRead(reader);
Backend backend = Backend.REGISTRY.get(id); Backend backend = Backend.REGISTRY.get(id);
if (backend == null) { if (backend == null) {
@ -38,6 +56,27 @@ public class BackendArgument implements ArgumentType<Backend> {
return backend; return backend;
} }
/**
* Copied from {@link ResourceLocation#read}, but defaults to flywheel namespace.
*/
@NotNull
private static ResourceLocation getRead(StringReader reader) throws CommandSyntaxException {
int i = reader.getCursor();
while(reader.canRead() && ResourceLocation.isAllowedInResourceLocation(reader.peek())) {
reader.skip();
}
String s = reader.getString().substring(i, reader.getCursor());
try {
return ResourceUtil.defaultToFlywheelNamespace(s);
} catch (ResourceLocationException resourcelocationexception) {
reader.setCursor(i);
throw ERROR_INVALID.createWithContext(reader);
}
}
@Override @Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) { public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
return SharedSuggestionProvider.suggest(STRING_IDS, builder); return SharedSuggestionProvider.suggest(STRING_IDS, builder);