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 8ee084136..b3a032337 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 @@ -29,6 +29,12 @@ public class IndirectInstancer extends AbstractInstancer private final AtomicReference pages; private final AtomicBitSet changedPages = new AtomicBitSet(); 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. + * These constraints are set so that we can guarantee that merging two pages leaves one entirely empty, + * but we also don't want to waste work merging into pages that are already empty. + */ + private final AtomicBitSet mergeablePages = new AtomicBitSet(); private final Class instanceClass; public ObjectStorage.@UnknownNullability Mapping mapping; @@ -116,6 +122,13 @@ public boolean add(I instance, InstanceHandleImpl handle) { // This is safe because only one bit position changes at a time. fullPages.set(pageNo); } + if (isEmpty(currentValue)) { + // Value we just saw was zero, so since we added something we are now mergeable! + mergeablePages.set(pageNo); + } else if (Integer.bitCount(currentValue) == 16) { + // We just filled the 17th instance, so we are no longer mergeable. + mergeablePages.clear(pageNo); + } return true; } } @@ -143,8 +156,16 @@ public InstanceHandleImpl.State setDeleted(int index) { int newValue = currentValue & ~(1 << localIndex); if (valid.compareAndSet(currentValue, newValue)) { - fullPages.clear(pageNo); changedPages.set(pageNo); + if (isEmpty(newValue)) { + // If we decremented to zero then we're no longer mergeable. + mergeablePages.clear(pageNo); + } else if (Integer.bitCount(newValue) == 16) { + // If we decremented to 16 then we're now mergeable. + mergeablePages.set(pageNo); + } + // Set full page last so that other threads don't race to set the other bitsets. + fullPages.clear(pageNo); return InstanceHandleImpl.Deleted.instance(); } } @@ -161,17 +182,12 @@ public InstanceHandleImpl.State setVisible(InstanceHandleImpl handle, int return new InstanceHandleImpl.Hidden<>(recreate, instances[localIndex]); } - public int takeFrom(InstancePage other) { + public 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(); - // If the other page is empty, or we're full, we're done. - if (isEmpty(otherValid) || isFull(valid)) { - return valid; - } - // Something is going to change, so mark stuff ahead of time. changedPages.set(pageNo); changedPages.set(other.pageNo); @@ -208,7 +224,13 @@ public int takeFrom(InstancePage other) { this.valid.set(valid); other.valid.set(otherValid); - return valid; + 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)); + + if (isFull(valid)) { + fullPages.set(pageNo); + } } } @@ -303,15 +325,36 @@ public void uploadInstances(StagingBuffer stagingBuffer, int instanceVbo) { } public void parallelUpdate() { - // TODO: Merge pages when they're less than half full. + var pages = this.pages.get(); + + int page = 0; + while (mergeablePages.cardinality() > 1) { + page = mergeablePages.nextSetBit(page); + if (page < 0) { + break; + } + + // Find the next mergeable page. + int next = mergeablePages.nextSetBit(page + 1); + if (next < 0) { + break; + } + + // Try to merge the pages. + pages[page].takeFrom(pages[next]); + } } private static boolean isFull(int valid) { return valid == 0xFFFFFFFF; } - private static boolean isEmpty(int otherValid) { - return otherValid == 0; + private static boolean isEmpty(int valid) { + return valid == 0; + } + + private static boolean isMergeable(int valid) { + return !isEmpty(valid) && Integer.bitCount(valid) <= 16; } @Override diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java b/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java index 334b02732..beeeb403d 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/util/AtomicBitSet.java @@ -43,6 +43,14 @@ public AtomicBitSet(int log2SegmentSizeInBits, int numBitsToPreallocate) { segments = new AtomicReference<>(new AtomicBitSetSegments(numSegmentsToPreallocate, numLongsPerSegment)); } + public void set(int position, boolean value) { + if (value) { + set(position); + } else { + clear(position); + } + } + public void set(int position) { int longPosition = longIndexInSegmentForPosition(position);