From b5680a0fd6199c3affbdcc722a020836a45a6d8b Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Tue, 3 Sep 2024 10:56:46 -0500 Subject: [PATCH] On-call paging - Only update the page table when an allocation is resized - Only upload the page table after it's uploaded - Combine various setters for InstancePager.Allocation and IndirectInstancer - Free pages when an allocation is deleted --- .../backend/engine/AbstractInstancer.java | 6 +- .../engine/indirect/IndirectBuffers.java | 4 +- .../engine/indirect/IndirectCullingGroup.java | 9 +- .../engine/indirect/IndirectInstancer.java | 22 +-- .../engine/indirect/InstancePager.java | 154 ++++++++++++------ 5 files changed, 113 insertions(+), 82 deletions(-) diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java index 744bb12a0..16aa88b64 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java @@ -127,7 +127,7 @@ public abstract class AbstractInstancer implements Instancer // We definitely shouldn't consider the deleted instances as changed though, // else we might try some out of bounds accesses later. - clearChangedRange(newSize, oldSize); + changed.clear(newSize, oldSize); // Punch out the deleted instances, shifting over surviving instances to fill their place. for (int scanPos = writePos; (scanPos < oldSize) && (writePos < newSize); scanPos++, writePos++) { @@ -155,10 +155,6 @@ public abstract class AbstractInstancer implements Instancer .clear(); } - protected void clearChangedRange(int start, int end) { - changed.clear(start, end); - } - protected void setRangeChanged(int start, int end) { changed.set(start, end); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java index 1ce962eec..193efd004 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java @@ -83,13 +83,13 @@ public class IndirectBuffers { draw.ensureCapacity(drawCount); final long ptr = multiBindBlock.ptr(); - MemoryUtil.memPutInt(ptr + INSTANCE_HANDLE_OFFSET, pageFile.storage.handle()); + MemoryUtil.memPutInt(ptr + INSTANCE_HANDLE_OFFSET, pageFile.objects.handle()); MemoryUtil.memPutInt(ptr + TARGET_HANDLE_OFFSET, target.handle()); MemoryUtil.memPutInt(ptr + MODEL_INDEX_HANDLE_OFFSET, pageFile.pageTable.handle()); MemoryUtil.memPutInt(ptr + MODEL_HANDLE_OFFSET, model.handle()); MemoryUtil.memPutInt(ptr + DRAW_HANDLE_OFFSET, draw.handle()); - MemoryUtil.memPutAddress(ptr + INSTANCE_SIZE_OFFSET, pageFile.storage.capacity()); + MemoryUtil.memPutAddress(ptr + INSTANCE_SIZE_OFFSET, pageFile.objects.capacity()); MemoryUtil.memPutAddress(ptr + TARGET_SIZE_OFFSET, INT_SIZE * instanceCount); MemoryUtil.memPutAddress(ptr + MODEL_INDEX_SIZE_OFFSET, pageFile.pageTable.capacity()); MemoryUtil.memPutAddress(ptr + MODEL_SIZE_OFFSET, MODEL_STRIDE * modelCount); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java index 8a25d5df5..da4f8cfe8 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java @@ -74,8 +74,7 @@ public class IndirectCullingGroup { continue; } - instancer.modelIndex(modelIndex); - instancer.baseInstance(instanceCountThisFrame); + instancer.postUpdate(modelIndex, instanceCountThisFrame); instanceCountThisFrame += instanceCount; modelIndex++; @@ -173,8 +172,8 @@ public class IndirectCullingGroup { } public void add(IndirectInstancer instancer, InstancerKey key, MeshPool meshPool) { - instancer.pageFile = buffers.pageFile.createPage(); - instancer.modelIndex(instancers.size()); + instancer.pageFile = buffers.pageFile.createAllocation(); + instancer.postUpdate(instancers.size(), -1); instancers.add(instancer); @@ -246,7 +245,7 @@ public class IndirectCullingGroup { private void uploadInstances(StagingBuffer stagingBuffer) { for (var instancer : instancers) { - instancer.uploadInstances(stagingBuffer, buffers.pageFile.storage.handle()); + instancer.uploadInstances(stagingBuffer, buffers.pageFile.objects.handle()); } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java index bbd78d88e..da825ed63 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java @@ -52,13 +52,6 @@ public class IndirectInstancer extends AbstractInstancer changedPages.set(InstancePager.object2Page(start), InstancePager.object2Page(end) + 1); } - @Override - protected void clearChangedRange(int start, int end) { - super.clearChangedRange(start, end); - - // changedPages.clear(pageFile.object2Page(start), pageFile); - } - public void addDraw(IndirectDraw draw) { associatedDraws.add(draw); } @@ -69,8 +62,12 @@ public class IndirectInstancer extends AbstractInstancer public void update() { removeDeletedInstances(); + } - pageFile.activeCount(instanceCount()); + public void postUpdate(int modelIndex, int baseInstance) { + this.modelIndex = modelIndex; + this.baseInstance = baseInstance; + pageFile.update(modelIndex, instanceCount()); } public void writeModel(long ptr) { @@ -117,21 +114,14 @@ public class IndirectInstancer extends AbstractInstancer for (IndirectDraw draw : draws()) { draw.delete(); } - } - public void modelIndex(int modelIndex) { - this.modelIndex = modelIndex; - pageFile.modelIndex(modelIndex); + pageFile.delete(); } public int modelIndex() { return modelIndex; } - public void baseInstance(int baseInstance) { - this.baseInstance = baseInstance; - } - public int baseInstance() { return baseInstance; } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/InstancePager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/InstancePager.java index 9c746be3d..a70b98321 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/InstancePager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/InstancePager.java @@ -1,8 +1,6 @@ package dev.engine_room.flywheel.backend.engine.indirect; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import org.lwjgl.system.MemoryUtil; @@ -17,24 +15,20 @@ public class InstancePager extends AbstractArena { public static final int INITIAL_PAGES_ALLOCATED = 4; - private final long objectSizeBytes; - - private MemoryBlock pageData; - - public final ResizableStorageBuffer storage; + private MemoryBlock pageTableData; + public final ResizableStorageBuffer objects; public final ResizableStorageBuffer pageTable; - private final List allocations = new ArrayList<>(); + private boolean needsUpload = false; public InstancePager(long objectSizeBytes) { super(PAGE_SIZE * objectSizeBytes); - this.objectSizeBytes = objectSizeBytes; - this.storage = new ResizableStorageBuffer(); + this.objects = new ResizableStorageBuffer(); this.pageTable = new ResizableStorageBuffer(); - pageData = MemoryBlock.malloc(INITIAL_PAGES_ALLOCATED * Integer.BYTES); - storage.ensureCapacity(INITIAL_PAGES_ALLOCATED * elementSizeBytes); + pageTableData = MemoryBlock.malloc(INITIAL_PAGES_ALLOCATED * Integer.BYTES); + objects.ensureCapacity(INITIAL_PAGES_ALLOCATED * elementSizeBytes); pageTable.ensureCapacity(INITIAL_PAGES_ALLOCATED * Integer.BYTES); } @@ -46,79 +40,122 @@ public class InstancePager extends AbstractArena { return pageIndex << LOG_2_PAGE_SIZE; } - public Allocation createPage() { - var out = new Allocation(); - allocations.add(out); - return out; + public Allocation createAllocation() { + return new Allocation(); } @Override public long byteCapacity() { - return storage.capacity(); + return objects.capacity(); + } + + @Override + public void free(int i) { + super.free(i); + MemoryUtil.memPutInt(ptrForPage(i), 0); } @Override protected void grow() { - pageData = pageData.realloc(pageData.size() * 2); - storage.ensureCapacity(storage.capacity() * 2); + pageTableData = pageTableData.realloc(pageTableData.size() * 2); + objects.ensureCapacity(objects.capacity() * 2); pageTable.ensureCapacity(pageTable.capacity() * 2); } public void uploadTable(StagingBuffer stagingBuffer) { - for (Allocation allocation : allocations) { - allocation.updatePageTable(); + if (!needsUpload) { + return; } - stagingBuffer.enqueueCopy(pageData.ptr(), pageData.size(), pageTable.handle(), 0); + // We could be smarter about which spans are uploaded but this thing is so small it's probably not worth it. + stagingBuffer.enqueueCopy(pageTableData.ptr(), pageTableData.size(), pageTable.handle(), 0); + needsUpload = false; } public void delete() { - storage.delete(); + objects.delete(); pageTable.delete(); - pageData.free(); + pageTableData.free(); + } + + private long ptrForPage(int page) { + return pageTableData.ptr() + (long) page * Integer.BYTES; } public class Allocation { - public int[] pages = new int[0]; + public static final int[] EMPTY_ALLOCATION = new int[0]; + public int[] pages = EMPTY_ALLOCATION; private int modelIndex = -1; - private int activeCount = 0; + private int objectCount = 0; - public void modelIndex(int modelIndex) { - if (this.modelIndex != modelIndex) { - this.modelIndex = modelIndex; + /** + * Calculates the page descriptor for the given page index. + * Runs under the assumption than all pages are full except maybe the last one. + */ + private int calculatePageDescriptor(int pageIndex) { + int countInPage; + if (objectCount % PAGE_SIZE != 0 && pageIndex == pages.length - 1) { + // Last page && it isn't full -> use the remainder. + countInPage = objectCount & PAGE_MASK; + } else if (objectCount > 0) { + // Full page. + countInPage = PAGE_SIZE; + } else { + // Empty page, this shouldn't be reachable because we eagerly free empty pages. + countInPage = 0; } + return (modelIndex & 0x3FFFFF) | (countInPage << 26); } - private void updatePageTable() { - if (pages.length == 0) { + public void update(int modelIndex, int objectCount) { + boolean incremental = this.modelIndex == modelIndex; + + if (incremental && objectCount == this.objectCount) { + // Nothing will change. return; } - var ptr = pageData.ptr(); + InstancePager.this.needsUpload = true; - int fullPage = (modelIndex & 0x3FFFFF) | (32 << 26); - - int remainder = activeCount; - - for (int i = 0; i < pages.length - 1; i++) { - int page = pages[i]; - MemoryUtil.memPutInt(ptr + page * Integer.BYTES, fullPage); - remainder -= PAGE_SIZE; - } - - MemoryUtil.memPutInt(ptr + pages[pages.length - 1] * Integer.BYTES, (modelIndex & 0x3FFFFF) | (remainder << 26)); - } - - public void activeCount(int objectCount) { - var neededPages = object2Page((objectCount + PAGE_MASK)); - activeCount = objectCount; + this.modelIndex = modelIndex; + this.objectCount = objectCount; var oldLength = pages.length; + var newLength = object2Page((objectCount + PAGE_MASK)); - if (oldLength > neededPages) { - shrink(oldLength, neededPages); - } else if (oldLength < neededPages) { - grow(neededPages, oldLength); + if (oldLength > newLength) { + // Eagerly free the now unnecessary pages. + // shrink will zero out the pageTable entries for the freed pages. + shrink(oldLength, newLength); + + if (incremental) { + // Only update the last page, everything else is unchanged. + updateRange(newLength - 1, newLength); + } + } else if (oldLength < newLength) { + // Allocate new pages to fit the new object count. + grow(newLength, oldLength); + + if (incremental) { + // Update the old last page + all new pages + updateRange(oldLength - 1, newLength); + } + } else { + if (incremental) { + // Only update the last page. + updateRange(oldLength - 1, oldLength); + } + } + + if (!incremental) { + // Update all pages. + updateRange(0, newLength); + } + } + + private void updateRange(int start, int oldLength) { + for (int i = start; i < oldLength; i++) { + MemoryUtil.memPutInt(ptrForPage(pages[i]), calculatePageDescriptor(i)); } } @@ -126,7 +163,8 @@ public class InstancePager extends AbstractArena { pages = Arrays.copyOf(pages, neededPages); for (int i = oldLength; i < neededPages; i++) { - pages[i] = InstancePager.this.alloc(); + var page = InstancePager.this.alloc(); + pages[i] = page; } } @@ -134,7 +172,6 @@ public class InstancePager extends AbstractArena { for (int i = oldLength - 1; i >= neededPages; i--) { var page = pages[i]; InstancePager.this.free(page); - MemoryUtil.memPutInt(pageData.ptr() + page * Integer.BYTES, 0); } pages = Arrays.copyOf(pages, neededPages); @@ -151,5 +188,14 @@ public class InstancePager extends AbstractArena { public long page2ByteOffset(int page) { return InstancePager.this.byteOffsetOf(pages[page]); } + + public void delete() { + for (int page : pages) { + InstancePager.this.free(page); + } + pages = EMPTY_ALLOCATION; + modelIndex = -1; + objectCount = 0; + } } }