Eviction notice

- For instancing, remove empty instancers and delete meshes
- Specify in InstancerProvider's contract that instancers should not be
  kept around, but reusing them within one frame in guaranteed to be
  safe
- Do all instancer updates in flush, and remove unnecessary checks that
  would be made later in the frame
- Remove isEmpty from Mesh
- Remove staging buffer param from indirect mesh pool
This commit is contained in:
Jozufozu 2024-02-19 10:53:52 -06:00
parent 593784d614
commit 7d59fdc86c
11 changed files with 132 additions and 135 deletions

View File

@ -9,7 +9,11 @@ public interface InstancerProvider {
/** /**
* Get an instancer for the given instance type rendering the given model. * Get an instancer for the given instance type rendering the given model.
* *
* <p>Calling this method twice with the same arguments will return the same instancer.</p> * <p>Calling this method twice with the same arguments in the
* same frame will return the same instancer.</p>
*
* <p>It is not safe to store instancers between frames. Each
* time you need an instancer, you should call this method.</p>
* *
* @return An instancer for the given instance type rendering the given model. * @return An instancer for the given instance type rendering the given model.
*/ */

View File

@ -13,14 +13,6 @@ public interface Mesh {
*/ */
int vertexCount(); int vertexCount();
/**
* Is there nothing to render?
* @return true if there are no vertices.
*/
default boolean isEmpty() {
return vertexCount() == 0;
}
/** /**
* Write this mesh into a vertex list. Vertices with index {@literal <}0 or {@literal >=}{@link #vertexCount()} will not be * Write this mesh into a vertex list. Vertices with index {@literal <}0 or {@literal >=}{@link #vertexCount()} will not be
* read or modified. * read or modified.

View File

@ -59,23 +59,23 @@ public abstract class InstancerStorage<N extends AbstractInstancer<?>> {
var out = create(key); var out = create(key);
// Only queue the instancer for initialization if it has anything to render. // Only queue the instancer for initialization if it has anything to render.
if (key.model() if (checkAndWarnEmptyModel(key.model())) {
.meshes()
.isEmpty()) {
warnEmptyModel();
} else {
// Thread safety: this method is called atomically from within computeIfAbsent, // Thread safety: this method is called atomically from within computeIfAbsent,
// so we don't need extra synchronization to protect the queue. // so we don't need extra synchronization to protect the queue.
initializationQueue.add(new UninitializedInstancer<>(key, out)); initializationQueue.add(new UninitializedInstancer<>(key, out));
} }
return out; return out;
} }
protected record UninitializedInstancer<N, I extends Instance>(InstancerKey<I> key, N instancer) { protected record UninitializedInstancer<N, I extends Instance>(InstancerKey<I> key, N instancer) {
} }
private static void warnEmptyModel() { private static boolean checkAndWarnEmptyModel(Model model) {
if (!model.meshes().isEmpty()) {
return true;
}
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("Creating an instancer for a model with no meshes! Stack trace:"); builder.append("Creating an instancer for a model with no meshes! Stack trace:");
@ -85,5 +85,7 @@ public abstract class InstancerStorage<N extends AbstractInstancer<?>> {
.append(f.toString())); .append(f.toString()));
Flywheel.LOGGER.warn(builder.toString()); Flywheel.LOGGER.warn(builder.toString());
return false;
} }
} }

View File

@ -83,7 +83,7 @@ public class IndirectCullingGroup<I extends Instance> {
buffers.updateCounts(instanceCountThisFrame, indirectModels.size(), indirectDraws.size()); buffers.updateCounts(instanceCountThisFrame, indirectModels.size(), indirectDraws.size());
// Must flush the mesh pool first so everything else has the right baseVertex and baseIndex. // Must flush the mesh pool first so everything else has the right baseVertex and baseIndex.
meshPool.flush(stagingBuffer); meshPool.flush();
// Upload only objects that have changed. // Upload only objects that have changed.
uploadObjects(stagingBuffer); uploadObjects(stagingBuffer);

View File

@ -62,15 +62,14 @@ public class IndirectMeshPool {
return meshes.get(mesh); return meshes.get(mesh);
} }
public void flush(StagingBuffer stagingBuffer) { public void flush() {
if (dirty) { if (dirty) {
// TODO: use the staging buffer and be smarter about allocation in general. uploadAll();
uploadAll(stagingBuffer);
dirty = false; dirty = false;
} }
} }
private void uploadAll(StagingBuffer stagingBuffer) { private void uploadAll() {
long neededSize = 0; long neededSize = 0;
int maxQuadIndexCount = 0; int maxQuadIndexCount = 0;
int nonQuadIndexCount = 0; int nonQuadIndexCount = 0;

View File

@ -11,32 +11,27 @@ public class DrawCall {
private final InstancedInstancer<?> instancer; private final InstancedInstancer<?> instancer;
private final InstancedMeshPool.BufferedMesh mesh; private final InstancedMeshPool.BufferedMesh mesh;
@Nullable private final GlVertexArray vao;
private GlVertexArray vao;
@Nullable @Nullable
private GlVertexArray vaoScratch; private GlVertexArray vaoScratch;
private boolean deleted;
public DrawCall(InstancedInstancer<?> instancer, InstancedMeshPool.BufferedMesh mesh, ShaderState shaderState) { public DrawCall(InstancedInstancer<?> instancer, InstancedMeshPool.BufferedMesh mesh, ShaderState shaderState) {
this.instancer = instancer; this.instancer = instancer;
this.mesh = mesh; this.mesh = mesh;
this.shaderState = shaderState; this.shaderState = shaderState;
mesh.acquire();
vao = GlVertexArray.create(); vao = GlVertexArray.create();
} }
public boolean isInvalid() { public boolean deleted() {
return instancer.isInvalid() || vao == null; return deleted;
} }
public void render() { public void render() {
if (isInvalid() || mesh.isEmpty()) { if (mesh.invalid()) {
return;
}
instancer.update();
int instanceCount = instancer.getInstanceCount();
if (instanceCount <= 0) {
return; return;
} }
@ -45,16 +40,14 @@ public class DrawCall {
vao.bindForDraw(); vao.bindForDraw();
mesh.draw(instanceCount); mesh.draw(instancer.getInstanceCount());
} }
public void renderOne(InstanceHandleImpl impl) { public void renderOne(InstanceHandleImpl impl) {
if (isInvalid() || mesh.isEmpty()) { if (mesh.invalid()) {
return; return;
} }
instancer.update();
int instanceCount = instancer.getInstanceCount(); int instanceCount = instancer.getInstanceCount();
if (instanceCount <= 0 || impl.index >= instanceCount) { if (instanceCount <= 0 || impl.index >= instanceCount) {
return; return;
@ -78,14 +71,19 @@ public class DrawCall {
} }
public void delete() { public void delete() {
if (vao != null) { if (deleted) {
vao.delete(); return;
vao = null;
} }
vao.delete();
if (vaoScratch != null) { if (vaoScratch != null) {
vaoScratch.delete(); vaoScratch.delete();
vaoScratch = null; vaoScratch = null;
} }
mesh.drop();
deleted = true;
} }
} }

View File

@ -98,11 +98,7 @@ public class InstancedCrumbling {
continue; continue;
} }
List<DrawCall> draws = instancer.drawCalls(); for (DrawCall draw : instancer.drawCalls()) {
draws.removeIf(DrawCall::isInvalid);
for (DrawCall draw : draws) {
out.computeIfAbsent(draw.shaderState, $ -> new Int2ObjectArrayMap<>()) out.computeIfAbsent(draw.shaderState, $ -> new Int2ObjectArrayMap<>())
.computeIfAbsent(progress, $ -> new ArrayList<>()) .computeIfAbsent(progress, $ -> new ArrayList<>())
.add(() -> draw.renderOne(impl)); .add(() -> draw.renderOne(impl));

View File

@ -10,7 +10,6 @@ import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap; 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.model.Mesh;
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.backend.engine.InstancerStorage;
@ -19,11 +18,11 @@ public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>
* The set of draw calls to make in each {@link RenderStage}. * The set of draw calls to make in each {@link RenderStage}.
*/ */
private final Map<RenderStage, DrawSet> drawSets = new EnumMap<>(RenderStage.class); private final Map<RenderStage, DrawSet> drawSets = new EnumMap<>(RenderStage.class);
private final EboCache eboCache = new EboCache();
/** /**
* A map of vertex types to their mesh pools. * A map of vertex types to their mesh pools.
*/ */
private final InstancedMeshPool meshPool = new InstancedMeshPool(); private final InstancedMeshPool meshPool = new InstancedMeshPool(eboCache);
private final EboCache eboCache = new EboCache();
public DrawSet get(RenderStage stage) { public DrawSet get(RenderStage stage) {
return drawSets.getOrDefault(stage, DrawSet.EMPTY); return drawSets.getOrDefault(stage, DrawSet.EMPTY);
@ -32,6 +31,24 @@ public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>
public void flush() { public void flush() {
super.flush(); super.flush();
var instancers = this.instancers.values();
instancers.removeIf(instancer -> {
// Update the instancers and remove any that are empty.
instancer.update();
if (instancer.getInstanceCount() == 0) {
instancer.delete();
return true;
} else {
return false;
}
});
for (DrawSet drawSet : drawSets.values()) {
// Remove the draw calls for any instancers we deleted.
drawSet.prune();
}
meshPool.flush(); meshPool.flush();
} }
@ -50,10 +67,6 @@ public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>
eboCache.invalidate(); eboCache.invalidate();
} }
private InstancedMeshPool.BufferedMesh alloc(Mesh mesh) {
return meshPool.alloc(mesh, eboCache);
}
@Override @Override
protected <I extends Instance> InstancedInstancer<I> create(InstancerKey<I> key) { protected <I extends Instance> InstancedInstancer<I> create(InstancerKey<I> key) {
return new InstancedInstancer<>(key.type(), key.context()); return new InstancedInstancer<>(key.type(), key.context());
@ -68,7 +81,7 @@ public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>
var meshes = key.model() var meshes = key.model()
.meshes(); .meshes();
for (var entry : meshes.entrySet()) { for (var entry : meshes.entrySet()) {
var mesh = alloc(entry.getValue()); var mesh = meshPool.alloc(entry.getValue());
ShaderState shaderState = new ShaderState(entry.getKey(), key.type(), key.context()); ShaderState shaderState = new ShaderState(entry.getKey(), key.type(), key.context());
DrawCall drawCall = new DrawCall(instancer, mesh, shaderState); DrawCall drawCall = new DrawCall(instancer, mesh, shaderState);
@ -111,5 +124,10 @@ public class InstancedDrawManager extends InstancerStorage<InstancedInstancer<?>
.entrySet() .entrySet()
.iterator(); .iterator();
} }
public void prune() {
drawCalls.values()
.removeIf(DrawCall::deleted);
}
} }
} }

View File

@ -39,14 +39,6 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
writer = type.writer(); writer = type.writer();
} }
public int getAttributeCount() {
return instanceAttributes.size();
}
public boolean isInvalid() {
return vbo == null;
}
public void init() { public void init() {
if (vbo != null) { if (vbo != null) {
return; return;
@ -58,28 +50,22 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
public void update() { public void update() {
removeDeletedInstances(); removeDeletedInstances();
ensureBufferCapacity();
updateBuffer(); updateBuffer();
} }
private void ensureBufferCapacity() {
int count = instances.size();
int byteSize = instanceStride * count;
if (vbo.ensureCapacity(byteSize)) {
// The vbo has moved, so we need to re-bind attributes
boundTo.clear();
}
}
private void updateBuffer() { private void updateBuffer() {
if (changed.isEmpty() || vbo == null) { if (changed.isEmpty() || vbo == null) {
return; return;
} }
try (MappedBuffer buf = vbo.map()) { int byteSize = instanceStride * instances.size();
long ptr = buf.ptr(); if (vbo.ensureCapacity(byteSize)) {
// The vbo has moved, so we need to re-bind attributes
boundTo.clear();
}
writeChanged(ptr); try (MappedBuffer buf = vbo.map()) {
writeChanged(buf.ptr());
changed.clear(); changed.clear();
} catch (Exception e) { } catch (Exception e) {
@ -129,6 +115,10 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
} }
vbo.delete(); vbo.delete();
vbo = null; vbo = null;
for (DrawCall drawCall : drawCalls) {
drawCall.delete();
}
} }
public void addDrawCall(DrawCall drawCall) { public void addDrawCall(DrawCall drawCall) {

View File

@ -21,11 +21,11 @@ import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
public class InstancedMeshPool { public class InstancedMeshPool {
private final VertexView vertexView; private final VertexView vertexView;
private final Map<Mesh, BufferedMesh> meshes = new HashMap<>(); private final Map<Mesh, BufferedMesh> byMesh = new HashMap<>();
private final List<BufferedMesh> allBuffered = new ArrayList<>(); private final List<BufferedMesh> ordered = new ArrayList<>();
private final List<BufferedMesh> pendingUpload = new ArrayList<>();
private final GlBuffer vbo; private final GlBuffer vbo;
private final EboCache eboCache;
private long byteSize; private long byteSize;
private boolean dirty; private boolean dirty;
@ -34,35 +34,34 @@ public class InstancedMeshPool {
/** /**
* Create a new mesh pool. * Create a new mesh pool.
*/ */
public InstancedMeshPool() { public InstancedMeshPool(EboCache eboCache) {
this.eboCache = eboCache;
vertexView = InternalVertex.createVertexView(); vertexView = InternalVertex.createVertexView();
int stride = InternalVertex.STRIDE; vbo = new GlBuffer();
vbo = new GlBuffer(); vbo.growthFunction(l -> Math.max(l + InternalVertex.STRIDE * 128L, (long) (l * 1.6)));
vbo.growthFunction(l -> Math.max(l + stride * 128L, (long) (l * 1.6)));
} }
/** /**
* Allocate a mesh in the arena. * Allocate a mesh in the arena.
* *
* @param mesh The mesh to allocate. * @param mesh The mesh to allocate.
* @param eboCache The EBO cache to use.
* @return A handle to the allocated mesh. * @return A handle to the allocated mesh.
*/ */
public BufferedMesh alloc(Mesh mesh, EboCache eboCache) { public BufferedMesh alloc(Mesh mesh) {
return meshes.computeIfAbsent(mesh, m -> { return byMesh.computeIfAbsent(mesh, this::_alloc);
BufferedMesh bufferedMesh = new BufferedMesh(m, byteSize, eboCache); }
byteSize += bufferedMesh.size();
allBuffered.add(bufferedMesh);
pendingUpload.add(bufferedMesh);
dirty = true; private BufferedMesh _alloc(Mesh m) {
return bufferedMesh; BufferedMesh bufferedMesh = new BufferedMesh(m, this.eboCache);
}); ordered.add(bufferedMesh);
dirty = true;
return bufferedMesh;
} }
@Nullable @Nullable
public BufferedMesh get(Mesh mesh) { public BufferedMesh get(Mesh mesh) {
return meshes.get(mesh); return byMesh.get(mesh);
} }
public void flush() { public void flush() {
@ -71,53 +70,59 @@ public class InstancedMeshPool {
} }
if (anyToRemove) { if (anyToRemove) {
anyToRemove = false;
processDeletions(); processDeletions();
} }
vbo.ensureCapacity(byteSize); var forUpload = calculateByteSizeAndGetMeshesForUpload();
uploadPending(); if (!forUpload.isEmpty()) {
vbo.ensureCapacity(byteSize);
upload(forUpload);
}
dirty = false; dirty = false;
pendingUpload.clear();
} }
private void processDeletions() { private void processDeletions() {
// remove deleted meshes // remove deleted meshes
allBuffered.removeIf(bufferedMesh -> { ordered.removeIf(bufferedMesh -> {
boolean deleted = bufferedMesh.isDeleted(); boolean deleted = bufferedMesh.deleted();
if (deleted) { if (deleted) {
meshes.remove(bufferedMesh.mesh); byMesh.remove(bufferedMesh.mesh);
} }
return deleted; return deleted;
}); });
}
// re-evaluate first vertex for each mesh private List<BufferedMesh> calculateByteSizeAndGetMeshesForUpload() {
int byteIndex = 0; List<BufferedMesh> out = new ArrayList<>();
for (BufferedMesh mesh : allBuffered) {
long byteIndex = 0;
for (BufferedMesh mesh : ordered) {
if (mesh.byteIndex != byteIndex) { if (mesh.byteIndex != byteIndex) {
pendingUpload.add(mesh); out.add(mesh);
} }
mesh.byteIndex = byteIndex; mesh.byteIndex = byteIndex;
byteIndex += mesh.size(); byteIndex += mesh.byteSize;
} }
this.byteSize = byteIndex; this.byteSize = byteIndex;
this.anyToRemove = false;
return out;
} }
private void uploadPending() { private void upload(List<BufferedMesh> meshes) {
try (MappedBuffer mapped = vbo.map()) { try (MappedBuffer mapped = vbo.map()) {
long ptr = mapped.ptr(); long ptr = mapped.ptr();
for (BufferedMesh mesh : pendingUpload) { for (BufferedMesh mesh : meshes) {
mesh.write(ptr, vertexView); mesh.write(ptr, vertexView);
mesh.boundTo.clear(); mesh.boundTo.clear();
} }
pendingUpload.clear();
} catch (Exception e) { } catch (Exception e) {
Flywheel.LOGGER.error("Error uploading pooled meshes:", e); Flywheel.LOGGER.error("Error uploading pooled meshes:", e);
} }
@ -125,14 +130,13 @@ public class InstancedMeshPool {
public void delete() { public void delete() {
vbo.delete(); vbo.delete();
meshes.clear(); byMesh.clear();
allBuffered.clear(); ordered.clear();
pendingUpload.clear();
} }
@Override @Override
public String toString() { public String toString() {
return "InstancedMeshPool{" + "byteSize=" + byteSize + ", meshCount=" + meshes.size() + '}'; return "InstancedMeshPool{" + "byteSize=" + byteSize + ", meshCount=" + byMesh.size() + '}';
} }
public class BufferedMesh { public class BufferedMesh {
@ -141,37 +145,28 @@ public class InstancedMeshPool {
private final int byteSize; private final int byteSize;
private final int ebo; private final int ebo;
private long byteIndex; private long byteIndex = -1;
private boolean deleted; private int referenceCount = 0;
private final Set<GlVertexArray> boundTo = new HashSet<>(); private final Set<GlVertexArray> boundTo = new HashSet<>();
private BufferedMesh(Mesh mesh, long byteIndex, EboCache eboCache) { private BufferedMesh(Mesh mesh, EboCache eboCache) {
this.mesh = mesh; this.mesh = mesh;
vertexCount = mesh.vertexCount(); vertexCount = mesh.vertexCount();
byteSize = vertexCount * InternalVertex.STRIDE; byteSize = vertexCount * InternalVertex.STRIDE;
this.byteIndex = byteIndex;
this.ebo = eboCache.get(mesh.indexSequence(), mesh.indexCount()); this.ebo = eboCache.get(mesh.indexSequence(), mesh.indexCount());
} }
public int vertexCount() { public boolean deleted() {
return vertexCount; return referenceCount <= 0;
} }
public int size() { public boolean invalid() {
return byteSize; return mesh.vertexCount() == 0 || deleted() || byteIndex == -1;
}
public boolean isDeleted() {
return deleted;
}
public boolean isEmpty() {
return mesh.isEmpty() || isDeleted();
} }
private void write(long ptr, VertexView vertexView) { private void write(long ptr, VertexView vertexView) {
if (isEmpty()) { if (invalid()) {
return; return;
} }
@ -196,10 +191,15 @@ public class InstancedMeshPool {
} }
} }
public void delete() { public void acquire() {
deleted = true; referenceCount++;
InstancedMeshPool.this.dirty = true; }
InstancedMeshPool.this.anyToRemove = true;
public void drop() {
if (--referenceCount == 0) {
InstancedMeshPool.this.dirty = true;
InstancedMeshPool.this.anyToRemove = true;
}
} }
} }
} }

View File

@ -94,8 +94,6 @@ public class InstancingEngine extends AbstractEngine {
var shader = entry.getKey(); var shader = entry.getKey();
var drawCalls = entry.getValue(); var drawCalls = entry.getValue();
drawCalls.removeIf(DrawCall::isInvalid);
if (drawCalls.isEmpty()) { if (drawCalls.isEmpty()) {
continue; continue;
} }