Better draw call organization

- InstancingEngine takes over tracking individual draw calls
 - Many draw calls are associated with a single ShaderState
 - Each ShaderState will bind one shader program
 - Make Material a record
 - Inline Renderable and move InstancedModel.Layer to DrawCall
This commit is contained in:
Jozufozu 2022-06-20 12:16:37 -07:00
parent 5dd72a4ba7
commit cdfddba35a
9 changed files with 185 additions and 203 deletions

View file

@ -4,26 +4,6 @@ import com.jozufozu.flywheel.core.source.FileResolution;
import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.RenderType;
public class Material { public record Material(RenderType renderType, FileResolution vertexShader, FileResolution fragmentShader) {
protected final RenderType renderType;
protected final FileResolution vertexShader;
protected final FileResolution fragmentShader;
public Material(RenderType renderType, FileResolution vertexShader, FileResolution fragmentShader) {
this.renderType = renderType;
this.vertexShader = vertexShader;
this.fragmentShader = fragmentShader;
}
public RenderType getRenderType() {
return renderType;
}
public FileResolution getVertexShader() {
return vertexShader;
}
public FileResolution getFragmentShader() {
return fragmentShader;
}
} }

View file

@ -0,0 +1,68 @@
package com.jozufozu.flywheel.backend.instancing.instancing;
import com.jozufozu.flywheel.api.material.Material;
import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.gl.GlStateTracker;
import com.jozufozu.flywheel.backend.gl.GlVertexArray;
import com.jozufozu.flywheel.backend.model.MeshPool;
import com.jozufozu.flywheel.core.model.Mesh;
public class DrawCall {
final Material material;
private final GPUInstancer<?> instancer;
MeshPool.BufferedMesh bufferedMesh;
GlVertexArray vao;
DrawCall(GPUInstancer<?> instancer, Material material, Mesh mesh) {
this.instancer = instancer;
this.material = material;
this.vao = new GlVertexArray();
this.bufferedMesh = MeshPool.getInstance()
.alloc(mesh);
this.instancer.attributeBaseIndex = this.bufferedMesh.getAttributeCount();
this.vao.enableArrays(this.bufferedMesh.getAttributeCount() + instancer.instanceFormat.getAttributeCount());
}
public Material getMaterial() {
return material;
}
public VertexType getVertexType() {
return bufferedMesh.getVertexType();
}
public void render() {
if (invalid()) return;
try (var ignored = GlStateTracker.getRestoreState()) {
this.instancer.renderSetup(vao);
if (this.instancer.glInstanceCount > 0) {
bufferedMesh.drawInstances(vao, this.instancer.glInstanceCount);
}
}
}
public boolean shouldRemove() {
return invalid();
}
/**
* Only {@code true} if the InstancedModel has been destroyed.
*/
private boolean invalid() {
return this.instancer.vbo == null || bufferedMesh == null || vao == null;
}
public void delete() {
if (invalid()) return;
vao.delete();
bufferedMesh.delete();
vao = null;
bufferedMesh = null;
}
}

View file

@ -1,15 +1,9 @@
package com.jozufozu.flywheel.backend.instancing.instancing; package com.jozufozu.flywheel.backend.instancing.instancing;
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 java.util.Set; import java.util.function.Consumer;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.jozufozu.flywheel.api.InstancedPart; import com.jozufozu.flywheel.api.InstancedPart;
import com.jozufozu.flywheel.api.Instancer; import com.jozufozu.flywheel.api.Instancer;
import com.jozufozu.flywheel.api.InstancerFactory; import com.jozufozu.flywheel.api.InstancerFactory;
@ -26,13 +20,11 @@ public class GPUInstancerFactory<D extends InstancedPart> implements InstancerFa
protected final Map<ModelSupplier, InstancedModel<D>> models = new HashMap<>(); protected final Map<ModelSupplier, InstancedModel<D>> models = new HashMap<>();
protected final StructType<D> type; protected final StructType<D> type;
private final Consumer<InstancedModel<D>> creationListener;
protected final List<InstancedModel<D>> uninitialized = new ArrayList<>(); public GPUInstancerFactory(StructType<D> type, Consumer<InstancedModel<D>> creationListener) {
private final ListMultimap<RenderType, Renderable> renderLists = ArrayListMultimap.create();
public GPUInstancerFactory(StructType<D> type) {
this.type = type; this.type = type;
this.creationListener = creationListener;
} }
@Override @Override
@ -58,7 +50,6 @@ public class GPUInstancerFactory<D extends InstancedPart> implements InstancerFa
public void delete() { public void delete() {
models.values().forEach(InstancedModel::delete); models.values().forEach(InstancedModel::delete);
models.clear(); models.clear();
renderLists.clear();
} }
/** /**
@ -71,35 +62,9 @@ public class GPUInstancerFactory<D extends InstancedPart> implements InstancerFa
.forEach(GPUInstancer::clear); .forEach(GPUInstancer::clear);
} }
public void init() {
for (var instanced : uninitialized) {
instanced.init();
for (Renderable renderable : instanced.getLayers()) {
renderLists.put(renderable.getMaterial()
.getRenderType(), renderable);
}
}
uninitialized.clear();
}
private InstancedModel<D> createInstancer(ModelSupplier model) { private InstancedModel<D> createInstancer(ModelSupplier model) {
var instancer = new InstancedModel<>(type, model); var instancer = new InstancedModel<>(type, model);
uninitialized.add(instancer); this.creationListener.accept(instancer);
return instancer; return instancer;
} }
/**
* Adds all the RenderTypes that this InstancerFactory will render to the given set.
* @param layersToProcess The set of RenderTypes that the InstancingEngine will process.
*/
public void gatherLayers(Set<RenderType> layersToProcess) {
layersToProcess.addAll(renderLists.keySet());
}
public List<Renderable> getRenderList(RenderType type) {
var out = renderLists.get(type);
out.removeIf(Renderable::shouldRemove);
return out;
}
} }

View file

@ -1,108 +1,35 @@
package com.jozufozu.flywheel.backend.instancing.instancing; package com.jozufozu.flywheel.backend.instancing.instancing;
import java.util.List; import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.jozufozu.flywheel.api.InstancedPart; import com.jozufozu.flywheel.api.InstancedPart;
import com.jozufozu.flywheel.api.material.Material;
import com.jozufozu.flywheel.api.struct.StructType; import com.jozufozu.flywheel.api.struct.StructType;
import com.jozufozu.flywheel.api.vertex.VertexType;
import com.jozufozu.flywheel.backend.gl.GlStateTracker;
import com.jozufozu.flywheel.backend.gl.GlVertexArray;
import com.jozufozu.flywheel.backend.model.MeshPool;
import com.jozufozu.flywheel.core.model.Mesh;
import com.jozufozu.flywheel.core.model.ModelSupplier; import com.jozufozu.flywheel.core.model.ModelSupplier;
import com.jozufozu.flywheel.util.Pair;
public class InstancedModel<D extends InstancedPart> { public class InstancedModel<D extends InstancedPart> {
public final GPUInstancer<D> instancer; public final GPUInstancer<D> instancer;
public final ModelSupplier model; public final ModelSupplier model;
private List<Layer> layers; private final StructType<D> type;
private List<DrawCall> layers;
public InstancedModel(StructType<D> type, ModelSupplier model) { public InstancedModel(StructType<D> type, ModelSupplier model) {
this.model = model; this.model = model;
this.instancer = new GPUInstancer<>(this, type); this.instancer = new GPUInstancer<>(this, type);
this.type = type;
} }
public void init() { public void init(RenderLists renderLists) {
instancer.init(); instancer.init();
buildLayers();
}
public List<? extends Renderable> getLayers() {
return layers;
}
private void buildLayers() {
layers = model.get() layers = model.get()
.entrySet() .entrySet()
.stream() .stream()
.map(entry -> new Layer(entry.getKey(), entry.getValue())) .map(entry -> new DrawCall(instancer, entry.getKey(), entry.getValue()))
.toList(); .toList();
}
private class Layer implements Renderable { for (DrawCall layer : layers) {
renderLists.add(new ShaderState(layer.material, layer.getVertexType(), type), layer);
final Material material;
MeshPool.BufferedMesh bufferedMesh;
GlVertexArray vao;
private Layer(Material material, Mesh mesh) {
this.material = material;
vao = new GlVertexArray();
bufferedMesh = MeshPool.getInstance()
.alloc(mesh);
instancer.attributeBaseIndex = bufferedMesh.getAttributeCount();
vao.enableArrays(bufferedMesh.getAttributeCount() + instancer.instanceFormat.getAttributeCount());
}
@Override
public Material getMaterial() {
return material;
}
@Override
public VertexType getVertexType() {
return bufferedMesh.getVertexType();
}
@Override
public void render() {
if (invalid()) return;
try (var ignored = GlStateTracker.getRestoreState()) {
instancer.renderSetup(vao);
if (instancer.glInstanceCount > 0) {
bufferedMesh.drawInstances(vao, instancer.glInstanceCount);
}
}
}
@Override
public boolean shouldRemove() {
return invalid();
}
/**
* Only {@code true} if the InstancedModel has been destroyed.
*/
private boolean invalid() {
return instancer.vbo == null || bufferedMesh == null || vao == null;
}
public void delete() {
if (invalid()) return;
vao.delete();
bufferedMesh.delete();
vao = null;
bufferedMesh = null;
} }
} }

View file

@ -2,16 +2,16 @@ package com.jozufozu.flywheel.backend.instancing.instancing;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.jozufozu.flywheel.api.InstancedPart; import com.jozufozu.flywheel.api.InstancedPart;
import com.jozufozu.flywheel.api.material.Material; import com.jozufozu.flywheel.api.material.Material;
import com.jozufozu.flywheel.api.struct.StructType; import com.jozufozu.flywheel.api.struct.StructType;
@ -34,6 +34,7 @@ import com.jozufozu.flywheel.core.crumbling.CrumblingProgram;
import com.jozufozu.flywheel.core.model.Mesh; import com.jozufozu.flywheel.core.model.Mesh;
import com.jozufozu.flywheel.core.model.ModelSupplier; import com.jozufozu.flywheel.core.model.ModelSupplier;
import com.jozufozu.flywheel.core.shader.WorldProgram; import com.jozufozu.flywheel.core.shader.WorldProgram;
import com.jozufozu.flywheel.core.source.FileResolution;
import com.jozufozu.flywheel.core.vertex.Formats; import com.jozufozu.flywheel.core.vertex.Formats;
import com.jozufozu.flywheel.mixin.LevelRendererAccessor; import com.jozufozu.flywheel.mixin.LevelRendererAccessor;
import com.jozufozu.flywheel.util.Textures; import com.jozufozu.flywheel.util.Textures;
@ -65,7 +66,8 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
protected final Map<StructType<? extends InstancedPart>, GPUInstancerFactory<?>> factories = new HashMap<>(); protected final Map<StructType<? extends InstancedPart>, GPUInstancerFactory<?>> factories = new HashMap<>();
protected final Set<RenderType> layersToProcess = new HashSet<>(); protected final List<InstancedModel<?>> uninitializedModels = new ArrayList<>();
protected final RenderLists renderLists = new RenderLists();
private final WeakHashSet<OriginShiftListener> listeners; private final WeakHashSet<OriginShiftListener> listeners;
private int vertexCount; private int vertexCount;
@ -81,7 +83,12 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
@NotNull @NotNull
@Override @Override
public <D extends InstancedPart> GPUInstancerFactory<D> factory(StructType<D> type) { public <D extends InstancedPart> GPUInstancerFactory<D> factory(StructType<D> type) {
return (GPUInstancerFactory<D>) factories.computeIfAbsent(type, GPUInstancerFactory::new); return (GPUInstancerFactory<D>) factories.computeIfAbsent(type, this::createFactory);
}
@NotNull
private <D extends InstancedPart> GPUInstancerFactory<D> createFactory(StructType<D> type) {
return new GPUInstancerFactory<>(type, uninitializedModels::add);
} }
@Override @Override
@ -94,15 +101,14 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
var vp = context.viewProjection().copy(); var vp = context.viewProjection().copy();
vp.multiplyWithTranslation((float) -camX, (float) -camY, (float) -camZ); vp.multiplyWithTranslation((float) -camX, (float) -camY, (float) -camZ);
for (RenderType renderType : layersToProcess) { for (RenderType renderType : renderLists.drainLayers()) {
render(renderType, camX, camY, camZ, vp, context.level()); render(renderType, camX, camY, camZ, vp, context.level());
} }
layersToProcess.clear();
} }
@Override @Override
public void renderSpecificType(TaskEngine taskEngine, RenderContext context, RenderType type) { public void renderSpecificType(TaskEngine taskEngine, RenderContext context, RenderType type) {
if (!layersToProcess.remove(type)) { if (!renderLists.process(type)) {
return; return;
} }
@ -121,37 +127,50 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
vertexCount = 0; vertexCount = 0;
instanceCount = 0; instanceCount = 0;
var multimap = renderLists.get(type);
if (multimap.isEmpty()) {
return;
}
render(type, multimap, camX, camY, camZ, viewProjection, level);
}
protected void render(RenderType type, ListMultimap<ShaderState, DrawCall> multimap, double camX, double camY, double camZ, Matrix4f viewProjection, ClientLevel level) {
type.setupRenderState(); type.setupRenderState();
Textures.bindActiveTextures(); Textures.bindActiveTextures();
CoreShaderInfo coreShaderInfo = CoreShaderInfo.get(); CoreShaderInfo coreShaderInfo = CoreShaderInfo.get();
for (var entry : factories.entrySet()) { for (var entry : Multimaps.asMap(multimap).entrySet()) {
var instanceType = entry.getKey(); var shader = entry.getKey();
var factory = entry.getValue(); var drawCalls = entry.getValue();
var toRender = factory.getRenderList(type); drawCalls.removeIf(DrawCall::shouldRemove);
if (toRender.isEmpty()) { if (drawCalls.isEmpty()) {
continue; continue;
} }
for (Renderable renderable : toRender) { setup(shader, coreShaderInfo, camX, camY, camZ, viewProjection, level);
setup(instanceType, renderable.getMaterial(), coreShaderInfo, camX, camY, camZ, viewProjection, level, renderable.getVertexType());
renderable.render(); for (var drawCall : drawCalls) {
drawCall.render();
} }
instanceCount += factory.getInstanceCount();
vertexCount += factory.getVertexCount();
} }
type.clearRenderState(); type.clearRenderState();
} }
protected P setup(StructType<?> instanceType, Material material, CoreShaderInfo coreShaderInfo, double camX, double camY, double camZ, Matrix4f viewProjection, ClientLevel level, VertexType vertexType) { protected P setup(ShaderState desc, CoreShaderInfo coreShaderInfo, double camX, double camY, double camZ, Matrix4f viewProjection, ClientLevel level) {
P program = context.getProgram(new ProgramCompiler.Context(vertexType, instanceType.getInstanceShader(), VertexType vertexType = desc.vertex();
material.getVertexShader(), material.getFragmentShader(), coreShaderInfo.getAdjustedAlphaDiscard(), FileResolution instanceShader = desc.instance()
.getInstanceShader();
Material material = desc.material();
P program = context.getProgram(new ProgramCompiler.Context(vertexType, instanceShader,
material.vertexShader(), material.fragmentShader(), coreShaderInfo.getAdjustedAlphaDiscard(),
coreShaderInfo.fogType(), GameStateRegistry.takeSnapshot())); coreShaderInfo.fogType(), GameStateRegistry.takeSnapshot()));
program.bind(); program.bind();
@ -190,12 +209,12 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
public void beginFrame(Camera info) { public void beginFrame(Camera info) {
checkOriginDistance(info); checkOriginDistance(info);
for (var factory : uninitializedModels) {
for (GPUInstancerFactory<?> factory : factories.values()) { factory.init(renderLists);
factory.init();
factory.gatherLayers(layersToProcess);
} }
uninitializedModels.clear();
renderLists.prepare();
MeshPool.getInstance() MeshPool.getInstance()
.flush(); .flush();
@ -279,13 +298,13 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
continue; continue;
} }
material.getRenderType().setupRenderState(); material.renderType().setupRenderState();
CoreShaderInfo coreShaderInfo = CoreShaderInfo.get(); CoreShaderInfo coreShaderInfo = CoreShaderInfo.get();
CrumblingProgram program = Contexts.CRUMBLING.getProgram(new ProgramCompiler.Context(Formats.POS_TEX_NORMAL, CrumblingProgram program = Contexts.CRUMBLING.getProgram(new ProgramCompiler.Context(Formats.POS_TEX_NORMAL,
structType.getInstanceShader(), material.getVertexShader(), material.getFragmentShader(), structType.getInstanceShader(), material.vertexShader(), material.fragmentShader(),
coreShaderInfo.getAdjustedAlphaDiscard(), coreShaderInfo.fogType(), coreShaderInfo.getAdjustedAlphaDiscard(), coreShaderInfo.fogType(),
GameStateRegistry.takeSnapshot())); GameStateRegistry.takeSnapshot()));

View file

@ -0,0 +1,49 @@
package com.jozufozu.flywheel.backend.instancing.instancing;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import net.minecraft.client.renderer.RenderType;
public class RenderLists {
private Map<RenderType, ListMultimap<ShaderState, DrawCall>> renderLists = new HashMap<>();
public final Set<RenderType> layersToProcess = new HashSet<>();
public ListMultimap<ShaderState, DrawCall> get(RenderType type) {
return renderLists.computeIfAbsent(type, k -> ArrayListMultimap.create());
}
public void add(ShaderState shaderState, DrawCall layer) {
RenderType renderType = shaderState.material()
.renderType();
get(renderType).put(shaderState, layer);
}
public void prepare() {
layersToProcess.clear();
layersToProcess.addAll(renderLists.keySet());
}
public Iterable<? extends RenderType> drainLayers() {
var out = new HashSet<>(layersToProcess);
layersToProcess.clear();
return out;
}
/**
* Check and mark a layer as processed.
* @param type The layer to check.
* @return {@code true} if the layer should be processed.
*/
public boolean process(RenderType type) {
return layersToProcess.remove(type);
}
}

View file

@ -1,15 +1,8 @@
package com.jozufozu.flywheel.backend.instancing.instancing; package com.jozufozu.flywheel.backend.instancing.instancing;
import com.jozufozu.flywheel.api.material.Material; import com.jozufozu.flywheel.api.material.Material;
import com.jozufozu.flywheel.api.struct.StructType;
import com.jozufozu.flywheel.api.vertex.VertexType; import com.jozufozu.flywheel.api.vertex.VertexType;
public interface Renderable { public record ShaderState(Material material, VertexType vertex, StructType<?> instance) {
void render();
boolean shouldRemove();
Material getMaterial();
VertexType getVertexType();
} }

View file

@ -9,14 +9,11 @@ import com.jozufozu.flywheel.backend.gl.GlStateTracker;
import com.jozufozu.flywheel.backend.instancing.InstanceManager; import com.jozufozu.flywheel.backend.instancing.InstanceManager;
import com.jozufozu.flywheel.backend.instancing.SerialTaskEngine; import com.jozufozu.flywheel.backend.instancing.SerialTaskEngine;
import com.jozufozu.flywheel.backend.instancing.instancing.InstancingEngine; import com.jozufozu.flywheel.backend.instancing.instancing.InstancingEngine;
import com.jozufozu.flywheel.backend.instancing.instancing.Renderable;
import com.jozufozu.flywheel.core.Contexts; import com.jozufozu.flywheel.core.Contexts;
import com.jozufozu.flywheel.core.CoreShaderInfoMap.CoreShaderInfo;
import com.jozufozu.flywheel.core.RenderContext; import com.jozufozu.flywheel.core.RenderContext;
import com.jozufozu.flywheel.event.ReloadRenderersEvent; import com.jozufozu.flywheel.event.ReloadRenderersEvent;
import com.jozufozu.flywheel.mixin.LevelRendererAccessor; import com.jozufozu.flywheel.mixin.LevelRendererAccessor;
import com.jozufozu.flywheel.util.Lazy; import com.jozufozu.flywheel.util.Lazy;
import com.jozufozu.flywheel.util.Textures;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Matrix4f; import com.mojang.math.Matrix4f;
@ -168,30 +165,13 @@ public class CrumblingRenderer {
return; return;
} }
currentLayer.setupRenderState(); var multimap = renderLists.get(type);
Textures.bindActiveTextures();
CoreShaderInfo coreShaderInfo = CoreShaderInfo.get();
for (var entry : factories.entrySet()) {
var instanceType = entry.getKey();
var factory = entry.getValue();
var toRender = factory.getRenderList(type);
if (toRender.isEmpty()) {
continue;
}
for (var renderable : toRender) {
setup(instanceType, renderable.getMaterial(), coreShaderInfo, camX, camY, camZ, viewProjection, level, renderable.getVertexType());
renderable.render();
}
if (multimap.isEmpty()) {
return;
} }
currentLayer.clearRenderState(); render(currentLayer, multimap, camX, camY, camZ, viewProjection, level);
} }
} }
} }

View file

@ -37,8 +37,9 @@ import net.minecraft.world.level.block.state.properties.ChestType;
public class ChestInstance<T extends BlockEntity & LidBlockEntity> extends BlockEntityInstance<T> implements DynamicInstance { public class ChestInstance<T extends BlockEntity & LidBlockEntity> extends BlockEntityInstance<T> implements DynamicInstance {
private static final BiFunction<ChestType, Material, BasicModelSupplier> LID = Util.memoize((type, mat) -> new BasicModelSupplier(() -> createLidModel(type, mat.sprite()), new com.jozufozu.flywheel.api.material.Material(Sheets.chestSheet(), MaterialShaders.SHADED_VERTEX, MaterialShaders.DEFAULT_FRAGMENT))); private static final com.jozufozu.flywheel.api.material.Material CHEST_MATERIAL = new com.jozufozu.flywheel.api.material.Material(Sheets.chestSheet(), MaterialShaders.SHADED_VERTEX, MaterialShaders.DEFAULT_FRAGMENT);
private static final BiFunction<ChestType, Material, BasicModelSupplier> BASE = Util.memoize((type, mat) -> new BasicModelSupplier(() -> createBaseModel(type, mat.sprite()), new com.jozufozu.flywheel.api.material.Material(Sheets.chestSheet(), MaterialShaders.SHADED_VERTEX, MaterialShaders.DEFAULT_FRAGMENT))); private static final BiFunction<ChestType, Material, BasicModelSupplier> LID = Util.memoize((type, mat) -> new BasicModelSupplier(() -> createLidModel(type, mat.sprite()), CHEST_MATERIAL));
private static final BiFunction<ChestType, Material, BasicModelSupplier> BASE = Util.memoize((type, mat) -> new BasicModelSupplier(() -> createBaseModel(type, mat.sprite()), CHEST_MATERIAL));
private final OrientedPart body; private final OrientedPart body;
private final TransformedPart lid; private final TransformedPart lid;