Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ArenaAggregator {
private static final long SHARED_INDEX_SIZE = MathUtil.fromMib(32);
private static final int DEFRAG_COPIES_PER_FRAME_BUDGET = 32;
private static final long DEFRAG_BYTES_PER_FRAME_BUDGET = MathUtil.fromMib(32);
private static final float MIN_FREE_FRACTION_AFTER_DEALLOC = 0.2f;
private static final float FREE_FRACTION_AFTER_DEALLOC_ABORT_LIMIT = 0.1f;

private static final GlBufferUsage BUFFER_USAGE = GlBufferUsage.STATIC_DRAW;

Expand Down Expand Up @@ -84,6 +86,10 @@ public int getUsedCopyCount() {
public long getUsedCopyBytes() {
return this.startCopyBytes - this.copyBytes;
}

public long getRemainingCopyBytes() {
return this.copyBytes;
}
}

private class DataType {
Expand All @@ -108,22 +114,22 @@ SharedGlBufferArena createSharedArena(CommandList commands, long requiredCapacit
}

SharedGlBufferArena ensureSharedArena(CommandList commands, long requiredCapacity) {
SharedGlBufferArena arena = null;
SharedGlBufferArena bestArena = null;
long biggestFreeSegmentSize = requiredCapacity;
for (var arenaEntry : this.arenas) {
long arenaBiggestFreeSegmentSize = arenaEntry.getBiggestFreeSegmentSize();
if (arenaBiggestFreeSegmentSize >= biggestFreeSegmentSize) {
arena = arenaEntry;
for (var arena : this.arenas) {
long arenaBiggestFreeSegmentSize = arena.getBiggestFreeSegmentSize();
if (!arena.isEmptying() && arenaBiggestFreeSegmentSize >= biggestFreeSegmentSize) {
bestArena = arena;
biggestFreeSegmentSize = arenaBiggestFreeSegmentSize;
}
}

if (arena == null) {
arena = createSharedArena(commands, Math.max(requiredCapacity, this.sharedSize));
this.arenas.add(arena);
if (bestArena == null) {
bestArena = createSharedArena(commands, Math.max(requiredCapacity, this.sharedSize));
this.arenas.add(bestArena);
}

return arena;
return bestArena;
}

long getDeviceUsedMemory() {
Expand All @@ -142,16 +148,73 @@ long getDeviceAllocatedMemory() {
return allocated;
}

void defragmentIncremental(CommandList commands, DefragBudget budget) {
void update(CommandList commands, DefragBudget budget) {
budget.setupElementCopy(this.stride);

// calculate total unfragmented free and capacity, remove empty arenas.
// note that this uses unfragmented free instead of calculating total free from total usage and total capacity because if we do this with fragmented free it might try to deallocate and arena that requires moving data but can't because there's not enough contiguous free space in the other arenas.
long totalCapacity = 0;
long totalUnfragmentedFree = 0;
SharedGlBufferArena emptyingArena = null;
var it = this.arenas.iterator();
while (it.hasNext()) {
var arena = it.next();

if (arena.isEmpty()) {
arena.deleteShared(commands);
it.remove();
continue;
}

totalCapacity += arena.getCapacity();
totalUnfragmentedFree += arena.getBiggestFreeSegmentSize();
if (arena.isEmptying()) {
emptyingArena = arena;
}
}

// perform emptying on the currently emptying arena
if (emptyingArena != null) {
// make sure the arena that's emptying wouldn't cause there to be too little free space
if (emptyingArena.getGlobalFreeFractionAfterEmptying(totalCapacity, totalUnfragmentedFree) < FREE_FRACTION_AFTER_DEALLOC_ABORT_LIMIT) {
emptyingArena.setEmptying(false);
emptyingArena = null;
}
// remove if emptying results in empty
else if (emptyingArena.continueEmptying(commands, budget)) {
emptyingArena.deleteShared(commands);
this.arenas.remove(emptyingArena);
emptyingArena = null;
}
}

// stop if the budget has been used up
if (emptyingArena != null && budget.isElementBudgetEmpty()) {
return;
}

// run defragmentation and find the least used arena that's not currently emptying to potentially empty
SharedGlBufferArena leastUsedArena = null;
for (int i = 0; i < this.arenas.size(); i++) {
int arenaIndex = (ArenaAggregator.this.arenaDefragOffset + i) % this.arenas.size();
var arenaEntry = this.arenas.get(arenaIndex);
arenaEntry.defragmentIncremental(commands, budget);
if (budget.isElementBudgetEmpty()) {
break;
var arena = this.arenas.get(arenaIndex);

if (!arena.isEmptying()) {
if (leastUsedArena == null || arena.getUsed() < leastUsedArena.getUsed()) {
leastUsedArena = arena;
}

if (!budget.isElementBudgetEmpty()) {
arena.defragmentIncremental(commands, budget);
}
}
}

// check if we can deallocate the least used arena by relocating its data into the others
if (emptyingArena == null && leastUsedArena != null && this.arenas.size() > 1 &&
leastUsedArena.getGlobalFreeFractionAfterEmptying(totalCapacity, totalUnfragmentedFree) >= MIN_FREE_FRACTION_AFTER_DEALLOC) {
leastUsedArena.setEmptying(true);
}
}
}

Expand Down Expand Up @@ -271,10 +334,7 @@ public void update(CommandList commands) {
for (int i = 0; i < this.dataTypes.size(); i++) {
int dataTypeIndex = (typeOffset + i) % this.dataTypes.size();
var dataType = this.dataTypes.get(dataTypeIndex);
dataType.defragmentIncremental(commands, budget);
if (budget.isBudgetEmpty()) {
break;
}
dataType.update(commands, budget);
}

this.totalCopyCount += budget.getUsedCopyCount();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package net.caffeinemc.mods.sodium.client.gl.arena;

import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import net.caffeinemc.mods.sodium.client.gl.buffer.GlMutableBuffer;
import net.caffeinemc.mods.sodium.client.gl.device.CommandList;
import net.minecraft.client.Minecraft;
Expand Down Expand Up @@ -39,7 +38,7 @@ protected void removeFreeSegment(GlBufferSegment segment) {
}

public long getBiggestFreeSegmentSize() {
return this.freeSegmentsByLength.getHighestSize();
return this.freeSegmentsByLength.getLargestSize();
}

private float calculateFragmentationDegree(Collection<GlBufferSegment> givenSegments) {
Expand Down Expand Up @@ -298,6 +297,8 @@ private boolean defragmentLeftwards(CommandList commands, GlBufferSegment bigges
// TODO: in weird rare cases this results in a negative offset, why?
// run with asserts enabled in mangrove forest: the overlapping segments are probably the cause
if (freeOffset < totalMoveLength) {
CHECK_ASSERTIONS = true;
this.checkAssertions();
throw new IllegalStateException("Invalid segments resulted in negative offset during defragmentation");
}
biggestFree.setOffset(freeOffset - totalMoveLength);
Expand Down Expand Up @@ -332,7 +333,7 @@ private boolean defragmentLeftwards(CommandList commands, GlBufferSegment bigges

@Override
GlBufferSegment takeFree(long size) {
return this.freeSegmentsByLength.removeFirstFitting(size);
return this.freeSegmentsByLength.removeFirstOfSizeAtLeast(size);
}

GlBufferSegment alloc(long size, RegionAllocatorHandle owner, int ownerIndex) {
Expand All @@ -354,6 +355,11 @@ GlBufferSegment alloc(long size, RegionAllocatorHandle owner, int ownerIndex) {
}
// free space is larger than requested, return new segment at end of free space
else {
if (free.getEnd() < size) {
CHECK_ASSERTIONS = true;
this.checkAssertions();
throw new IllegalStateException("Free segment is smaller than requested size");
}
result = new GlBufferSegment(this, owner, ownerIndex, free.getEnd() - size, size);
result.setNext(free.getNext());
result.setPrev(free);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.stream.Stream;

public class GlBufferArena implements AllocatorBase {
static final boolean CHECK_ASSERTIONS = false;
public static boolean CHECK_ASSERTIONS = false;

// how many segments we require to be present before we calculate an average size
public static final int MIN_SEGMENTS_FOR_AVG = 16;
Expand Down Expand Up @@ -87,7 +87,7 @@ private void transferSegments(CommandList commandList, Collection<PendingBufferC
this.capacity = this.arenaBuffer.getSize() / this.stride;
}

void receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments, GlMutableBuffer srcBufferObj, RegionAllocatorHandle owner) {
int receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments, GlMutableBuffer srcBufferObj, RegionAllocatorHandle owner) {
this.used = owner.used;
this.usedSegments = segments.size();
if (this.used > this.capacity) {
Expand All @@ -105,6 +105,8 @@ void receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments
this.executeCopyCommands(commandList, pendingCopies, srcBufferObj, this.arenaBuffer);

this.finalizeCompactedSegments(endOfFreeHead, segments);

return pendingCopies.size();
}

private void finalizeCompactedSegments(long tail, List<GlBufferSegment> usedSegments) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

public class SharedGlBufferArena extends DefragmentingGlBufferArena implements SizedTreeMap.Sized {
final SizedTreeMap<RegionAllocatorHandle> ownersByUsed = new SizedTreeMap<>();
private boolean isEmptying = false;

private final int identifier;
private static int nextIdentifier = 1;
Expand Down Expand Up @@ -36,6 +37,26 @@ public void deleteShared(CommandList commands) {
super.deleteSingleOwner(commands);
}

public long getUsed() {
return this.used;
}

public long getCapacity() {
return this.capacity;
}

public boolean isEmpty() {
return this.used == 0;
}

public boolean isEmptying() {
return this.isEmptying;
}

public void setEmptying(boolean emptying) {
this.isEmptying = emptying;
}

@Override
void updateUsed(long deltaUsed, RegionAllocatorHandle owner) {
this.ownersByUsed.removeSized(owner);
Expand All @@ -49,11 +70,54 @@ void updateUsed(long deltaUsed, RegionAllocatorHandle owner) {
this.ownersByUsed.addSized(owner);
}

public float getGlobalFreeFractionAfterEmptying(long totalCapacity, long totalUnfragmentedFree) {
long leastUsedFree = this.capacity - this.used;
long otherFree = totalUnfragmentedFree - leastUsedFree;
long remainingFreeAfterDealloc = otherFree - this.used;
long remainingCapacityAfterDealloc = totalCapacity - this.capacity;
return (float) remainingFreeAfterDealloc / remainingCapacityAfterDealloc;
}

public boolean continueEmptying(CommandList commands, ArenaAggregator.DefragBudget budget) {
// TODO: if eviction requires creating a new arena due to fragmentation/sizing, stop emptying.
if (!this.isEmptying) {
throw new IllegalStateException("Arena is not emptying");
}
if (this.isEmpty()) {
return true;
}

// get the biggest owner that fits into the budget
var ownerToEvict = this.ownersByUsed.removeLargestOfSizeAtMost(budget.getRemainingCopyBytes());
if (ownerToEvict == null) {
return false;
}

var copyCount = estimateAndTransferOwner(commands, ownerToEvict);

// notify the owner that has been moved of the buffer change
ownerToEvict.notifyBufferChanged(commands);

budget.consumeElementCopy(ownerToEvict.used, copyCount);

return this.isEmpty();
}

private int estimateAndTransferOwner(CommandList commands, RegionAllocatorHandle ownerToEvict) {
return this.estimateAndTransferUploadingOwner(commands, ownerToEvict.usedSegments, ownerToEvict, ownerToEvict.used);
}

private int estimateAndTransferUploadingOwner(CommandList commands, int finalSegmentCount, RegionAllocatorHandle biggestUsageOwner, long finalUsage) {
// TODO: when estimating new capacity, take into account how full the section already is since a full section will not grow much anymore
var newCapacity = GlBufferArena.estimateNewCapacity(finalSegmentCount, biggestUsageOwner.getFillFractionInv(), finalUsage);
return this.transferOwnerToNewArena(commands, biggestUsageOwner, newCapacity);
}

@Override
void handleResizeUploads(CommandList commands, RegionAllocatorHandle uploadingOwner, List<PendingUpload> queue, long totalOwnerUsageAfterUploads) {
boolean relocatedUploadingOwner = false;

// this needs to be a loop because the young gen buffer isn't guaranteed to be defragmented so we may need to evict more than one owner
// this needs to be a loop because the shared buffer isn't guaranteed to be fully defragmented so we may need to evict more than one owner
do {
long biggestUsage = Long.MAX_VALUE; // max value to make sure the head map lookup works
int biggestUsageSegmentCount = 0;
Expand All @@ -66,12 +130,11 @@ void handleResizeUploads(CommandList commands, RegionAllocatorHandle uploadingOw
}

// check if there's another owner that is bigger than this owner will be when it is fully uploaded
for (var entry : this.ownersByUsed.reversed().entrySet()) {
var entryOwner = entry.getValue();
if (entryOwner != uploadingOwner && entryOwner.used > biggestUsage) {
biggestUsage = entryOwner.used;
biggestUsageSegmentCount = entryOwner.usedSegments;
biggestUsageOwner = entryOwner;
for (var otherOwner : this.ownersByUsed.reversed().values()) {
if (otherOwner != uploadingOwner && otherOwner.used > biggestUsage) {
biggestUsage = otherOwner.used;
biggestUsageSegmentCount = otherOwner.usedSegments;
biggestUsageOwner = otherOwner;
break;
}
}
Expand All @@ -81,17 +144,14 @@ void handleResizeUploads(CommandList commands, RegionAllocatorHandle uploadingOw
}

// by construction, either the owner is the biggest one and is getting moved to its own arena, or another owner is bigger and this one will fit into this young gen arena

// TODO: when estimating new capacity, take into account how full the section already is since a full section will not grow much anymore
var newCapacity = GlBufferArena.estimateNewCapacity(biggestUsageSegmentCount, biggestUsageOwner.getFillFractionInv(), biggestUsage);
transferToNewArena(commands, biggestUsageOwner, newCapacity);
estimateAndTransferUploadingOwner(commands, biggestUsageSegmentCount, biggestUsageOwner, biggestUsage);

// try uploading again
uploadingOwner.getBackingArena().tryUploads(commands, uploadingOwner, queue);
} while (!queue.isEmpty());
}

private void transferToNewArena(CommandList commands, RegionAllocatorHandle owner, long newCapacity) {
private int transferOwnerToNewArena(CommandList commands, RegionAllocatorHandle owner, long newCapacity) {
var targetArena = this.parent.getArenaFittingFor(commands, newCapacity, this.stride);
if (targetArena == this) {
throw new IllegalStateException("Target arena is the same as the source arena");
Expand All @@ -106,14 +166,16 @@ private void transferToNewArena(CommandList commands, RegionAllocatorHandle owne
this.checkAssertions();

// copy the extracted segments into the new arena
targetArena.receiveSegmentsFrom(commands, extractedSegments, this.arenaBuffer, owner);
var copyCount = targetArena.receiveSegmentsFrom(commands, extractedSegments, this.arenaBuffer, owner);

// notify the owner that has been moved of the buffer change
owner.notifyBufferChanged(commands);

return copyCount;
}

@Override
void receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments, GlMutableBuffer srcBufferObj, RegionAllocatorHandle owner) {
int receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments, GlMutableBuffer srcBufferObj, RegionAllocatorHandle owner) {
this.used += owner.used;
this.usedSegments += segments.size();
if (this.used > this.capacity) {
Expand All @@ -133,6 +195,8 @@ void receiveSegmentsFrom(CommandList commandList, List<GlBufferSegment> segments
this.executeCopyCommands(commandList, pendingCopies, srcBufferObj, this.arenaBuffer);

this.finalizeInsertedSegments(targetSegment, endOfFreePrefix, segments);

return pendingCopies.size();
}

private void finalizeInsertedSegments(GlBufferSegment targetSegment, long endOfFreePrefix, List<GlBufferSegment> segments) {
Expand Down
Loading