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
This commit is contained in:
Jozufozu 2024-11-02 14:01:22 -07:00
parent fc3e475ec9
commit a9f2018c0a
2 changed files with 76 additions and 36 deletions

View file

@ -27,7 +27,19 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
private final Vector4fc boundingSphere;
private final AtomicReference<InstancePage[]> pages;
private final AtomicBitSet changedPages = new AtomicBitSet();
/**
* 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.
@ -69,6 +81,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
public final class InstancePage implements InstanceHandleImpl.State<I> {
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.
@ -116,7 +129,8 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
// Handle index is unique amongst all pages of this instancer.
handle.index = local2HandleIndex(index);
changedPages.set(pageNo);
contentsChanged.set(pageNo);
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.
@ -140,7 +154,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
@Override
public InstanceHandleImpl.State<I> setChanged(int index) {
changedPages.set(pageNo);
contentsChanged.set(pageNo);
return this;
}
@ -156,7 +170,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
int newValue = currentValue & ~(1 << localIndex);
if (valid.compareAndSet(currentValue, newValue)) {
changedPages.set(pageNo);
validityChanged.set(pageNo);
if (isEmpty(newValue)) {
// If we decremented to zero then we're no longer mergeable.
mergeablePages.clear(pageNo);
@ -182,19 +196,21 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
return new InstanceHandleImpl.Hidden<>(recreate, instances[localIndex]);
}
public void takeFrom(InstancePage other) {
/**
* Only call this on 2 pages that are mergeable.
*
* @param other The page to take instances from.
*/
private void takeFrom(InstancePage other) {
// Fill the holes in this page with instances from the other page.
int valid = this.valid.get();
int otherValid = other.valid.get();
// Something is going to change, so mark stuff ahead of time.
changedPages.set(pageNo);
changedPages.set(other.pageNo);
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;
}
@ -212,7 +228,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
other.handles[i] = null;
other.instances[i] = null;
// Set the bit in this page and find the next write position.
// Set the bit in this page so we can find the next write position.
valid |= 1 << writePos;
// If we're full, we're done.
@ -224,9 +240,18 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
this.valid.set(valid);
other.valid.set(otherValid);
// If the other page was quite empty we may still be mergeable.
mergeablePages.set(pageNo, isMergeable(valid));
// With all assumptions in place we should have fully drained the other page, but check just to be safe.
mergeablePages.set(other.pageNo, isMergeable(otherValid));
// We definitely changed the contents and validity of this page.
contentsChanged.set(pageNo);
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.
contentsChanged.clear(other.pageNo);
validityChanged.set(other.pageNo);
mergeablePages.clear(other.pageNo);
if (isFull(valid)) {
fullPages.set(pageNo);
@ -246,7 +271,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
this.baseInstance = baseInstance;
var sameModelIndex = this.modelIndex == modelIndex;
if (sameModelIndex && changedPages.isEmpty()) {
if (sameModelIndex && validityChanged.isEmpty()) {
// Nothing to do!
return;
}
@ -258,7 +283,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
if (sameModelIndex) {
// Only need to update the changed pages.
for (int page = changedPages.nextSetBit(0); page >= 0 && page < pages.length; page = changedPages.nextSetBit(page + 1)) {
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 {
@ -267,6 +292,8 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
mapping.updatePage(i, modelIndex, pages[i].valid.get());
}
}
validityChanged.clear();
}
public void writeModel(long ptr) {
@ -280,15 +307,21 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
}
public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) {
if (changedPages.isEmpty()) {
if (contentsChanged.isEmpty()) {
return;
}
var pages = this.pages.get();
for (int page = changedPages.nextSetBit(0); page >= 0 && page < pages.length; page = changedPages.nextSetBit(page + 1)) {
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);
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
@ -321,7 +354,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
stagingBuffer.enqueueCopy(block.ptr(), size, instanceVbo, baseByte);
}
changedPages.clear();
contentsChanged.clear();
}
public void parallelUpdate() {
@ -501,7 +534,7 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
*/
public void clear() {
this.pages.set(pageArray(0));
changedPages.clear();
contentsChanged.clear();
fullPages.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;
@ -18,6 +19,7 @@ public class ObjectStorage extends AbstractArena {
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.
*/
@ -31,8 +33,6 @@ public class ObjectStorage extends AbstractArena {
*/
private MemoryBlock frameDescriptors;
private boolean needsUpload = false;
public ObjectStorage(long objectSizeBytes) {
super(PAGE_SIZE * objectSizeBytes);
@ -62,6 +62,16 @@ public class ObjectStorage extends AbstractArena {
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
@ -72,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() {
@ -110,18 +125,14 @@ public class ObjectStorage extends AbstractArena {
holePunch(index);
return;
}
var page = pages[index];
var frame = pages[index];
if (page == INVALID_PAGE) {
if (frame == INVALID_PAGE) {
// Un-holed punch.
page = unHolePunch(index);
frame = unHolePunch(index);
}
var ptr = ptrForPage(page);
MemoryUtil.memPutInt(ptr, modelIndex);
MemoryUtil.memPutInt(ptr + 4, validBits);
ObjectStorage.this.needsUpload = true;
ObjectStorage.this.set(frame, modelIndex, validBits);
}
/**
@ -132,8 +143,6 @@ public class ObjectStorage extends AbstractArena {
public void holePunch(int index) {
ObjectStorage.this.free(pages[index]);
pages[index] = INVALID_PAGE;
ObjectStorage.this.needsUpload = true;
}
/**
@ -173,8 +182,6 @@ public class ObjectStorage extends AbstractArena {
ObjectStorage.this.free(page);
}
pages = EMPTY_ALLOCATION;
ObjectStorage.this.needsUpload = true;
}
private void grow(int neededPages, int oldLength) {