Rewrite dynamic_sounds to consider sound duration when caching
Related: #594
This commit is contained in:
parent
8133198cc2
commit
273bab7856
|
|
@ -1,47 +1,29 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.dynamic_sounds;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.RemovalCause;
|
||||
import com.google.common.cache.RemovalNotification;
|
||||
import com.mojang.blaze3d.audio.SoundBuffer;
|
||||
import net.minecraft.client.sounds.SoundBufferLibrary;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
|
||||
import org.embeddedt.modernfix.dynamicresources.DynamicSoundHelpers;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Mutable;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Map;
|
||||
|
||||
@Mixin(SoundBufferLibrary.class)
|
||||
@ClientOnlyMixin
|
||||
public abstract class SoundBufferLibraryMixin {
|
||||
|
||||
private static final boolean debugDynamicSoundLoading = Boolean.getBoolean("modernfix.debugDynamicSoundLoading");
|
||||
|
||||
@Shadow @Final @Mutable
|
||||
private Map<ResourceLocation, CompletableFuture<SoundBuffer>> cache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(DynamicSoundHelpers.MAX_SOUND_LIFETIME_SECS, TimeUnit.SECONDS)
|
||||
.concurrencyLevel(1)
|
||||
// Excessive use of type hinting due to it assuming Object as the broadest correct type
|
||||
.<ResourceLocation, CompletableFuture<SoundBuffer>>removalListener(this::onSoundRemoval)
|
||||
.build()
|
||||
.asMap();
|
||||
private Map<ResourceLocation, CompletableFuture<SoundBuffer>> cache;
|
||||
|
||||
private <K extends ResourceLocation, V extends CompletableFuture<SoundBuffer>> void onSoundRemoval(RemovalNotification<K, V> notification) {
|
||||
if(notification.getCause() == RemovalCause.REPLACED && notification.getValue() == cache.get(notification.getKey()))
|
||||
return;
|
||||
notification.getValue().thenAccept(SoundBuffer::discardAlBuffer);
|
||||
if(!debugDynamicSoundLoading)
|
||||
return;
|
||||
K k = notification.getKey();
|
||||
if(k == null)
|
||||
return;
|
||||
ModernFix.LOGGER.warn("Evicted sound {}", k);
|
||||
@Inject(method = "<init>", at = @At("RETURN"))
|
||||
private void replaceCache(CallbackInfo ci) {
|
||||
this.cache = new DynamicSoundHelpers.Cache(cache);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.dynamic_sounds;
|
||||
|
||||
import com.mojang.blaze3d.audio.SoundBuffer;
|
||||
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
|
||||
import org.embeddedt.modernfix.dynamicresources.DynamicSoundHelpers;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ClientOnlyMixin
|
||||
@Mixin(SoundBuffer.class)
|
||||
public class SoundBufferMixin implements DynamicSoundHelpers.SoundBufAccess {
|
||||
@Unique
|
||||
private long mfix$durationNanos;
|
||||
|
||||
@Inject(method = "<init>", at = @At("RETURN"))
|
||||
private void computeDuration(ByteBuffer data, AudioFormat format, CallbackInfo ci) {
|
||||
if (data != null) {
|
||||
int numFrames = data.capacity() / format.getFrameSize();
|
||||
double seconds = ((double)numFrames / format.getFrameRate());
|
||||
mfix$durationNanos = Math.max(0, (long)Math.ceil(seconds * 1_000_000_000.0));
|
||||
} else {
|
||||
mfix$durationNanos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mfix$getDurationNanos() {
|
||||
return mfix$durationNanos;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,142 @@
|
|||
package org.embeddedt.modernfix.dynamicresources;
|
||||
|
||||
import com.mojang.blaze3d.audio.SoundBuffer;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.AbstractSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DynamicSoundHelpers {
|
||||
/**
|
||||
* The duration until a sound is eligible for eviction if unused.
|
||||
*/
|
||||
public static final int MAX_SOUND_LIFETIME_SECS = 300;
|
||||
private static final long SOUND_EVICTION_DELAY = TimeUnit.SECONDS.toNanos(30);
|
||||
private static final boolean debugDynamicSoundLoading = Boolean.getBoolean("modernfix.debugDynamicSoundLoading");
|
||||
|
||||
public interface SoundBufAccess {
|
||||
long mfix$getDurationNanos();
|
||||
}
|
||||
|
||||
public static final class Cache extends AbstractMap<ResourceLocation, CompletableFuture<SoundBuffer>> {
|
||||
private static class Entry {
|
||||
private final CompletableFuture<SoundBuffer> buffer;
|
||||
private long lastAccessTime;
|
||||
|
||||
private Entry(CompletableFuture<SoundBuffer> buffer) {
|
||||
this.buffer = buffer;
|
||||
this.lastAccessTime = System.nanoTime();
|
||||
}
|
||||
|
||||
public CompletableFuture<SoundBuffer> getBuffer() {
|
||||
this.lastAccessTime = System.nanoTime();
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
var buf = this.buffer.getNow(null);
|
||||
if (buf == null) {
|
||||
return 0;
|
||||
}
|
||||
return ((SoundBufAccess)buf).mfix$getDurationNanos();
|
||||
}
|
||||
|
||||
public boolean isExpired(long currentTs) {
|
||||
long duration = getDuration();
|
||||
return duration > 0 && (currentTs - this.lastAccessTime) >= (duration + SOUND_EVICTION_DELAY);
|
||||
}
|
||||
|
||||
public void discard() {
|
||||
this.buffer.thenAccept(SoundBuffer::discardAlBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private final Object2ObjectLinkedOpenHashMap<ResourceLocation, Entry> store = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
public Cache(Map<ResourceLocation, CompletableFuture<SoundBuffer>> otherMap) {
|
||||
this.putAll(otherMap);
|
||||
}
|
||||
|
||||
private void checkExpired() {
|
||||
long ts = System.nanoTime();
|
||||
var iter = this.store.object2ObjectEntrySet().fastIterator();
|
||||
while (iter.hasNext()) {
|
||||
var entry = iter.next();
|
||||
if (entry.getValue().isExpired(ts)) {
|
||||
if (debugDynamicSoundLoading) {
|
||||
ModernFix.LOGGER.warn("Evicted sound {} with duration {} ms", entry.getKey(), entry.getValue().getDuration() / 1000000);
|
||||
}
|
||||
entry.getValue().discard();
|
||||
iter.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SoundBuffer> get(Object key) {
|
||||
if (key instanceof ResourceLocation rl) {
|
||||
var entry = this.store.getAndMoveToLast(rl);
|
||||
CompletableFuture<SoundBuffer> result = entry != null ? entry.getBuffer() : null;
|
||||
this.checkExpired();
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SoundBuffer> put(ResourceLocation key, CompletableFuture<SoundBuffer> value) {
|
||||
var entry = new Entry(value);
|
||||
if (debugDynamicSoundLoading) {
|
||||
ModernFix.LOGGER.info("Loaded sound {}", key);
|
||||
}
|
||||
var previousEntry = this.store.putAndMoveToLast(key, entry);
|
||||
return previousEntry != null ? previousEntry.getBuffer() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<Map.Entry<ResourceLocation, CompletableFuture<SoundBuffer>>> entrySet() {
|
||||
return new EntrySet();
|
||||
}
|
||||
|
||||
private class EntrySet extends AbstractSet<Map.Entry<ResourceLocation, CompletableFuture<SoundBuffer>>> {
|
||||
@Override
|
||||
public Iterator<Map.Entry<ResourceLocation, CompletableFuture<SoundBuffer>>> iterator() {
|
||||
var storeIter = store.entrySet().iterator();
|
||||
return new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return storeIter.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<ResourceLocation, CompletableFuture<SoundBuffer>> next() {
|
||||
var entry = storeIter.next();
|
||||
return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue().buffer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return store.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
store.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package org.embeddedt.modernfix.sound;
|
||||
|
||||
import com.mojang.blaze3d.audio.SoundBuffer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class SoundBufferCache extends LinkedHashMap<ResourceLocation, CompletableFuture<SoundBuffer>> {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<ResourceLocation, CompletableFuture<SoundBuffer>> eldest) {
|
||||
return super.removeEldestEntry(eldest);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user