diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferLibraryMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferLibraryMixin.java index 6d5da6f6..74dec490 100644 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferLibraryMixin.java +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferLibraryMixin.java @@ -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> 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 - .>removalListener(this::onSoundRemoval) - .build() - .asMap(); + private Map> cache; - private > void onSoundRemoval(RemovalNotification 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 = "", at = @At("RETURN")) + private void replaceCache(CallbackInfo ci) { + this.cache = new DynamicSoundHelpers.Cache(cache); } } diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferMixin.java new file mode 100644 index 00000000..ad075170 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferMixin.java @@ -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 = "", 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; + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicSoundHelpers.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicSoundHelpers.java index 876fd47d..32c1df83 100644 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicSoundHelpers.java +++ b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicSoundHelpers.java @@ -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> { + private static class Entry { + private final CompletableFuture buffer; + private long lastAccessTime; + + private Entry(CompletableFuture buffer) { + this.buffer = buffer; + this.lastAccessTime = System.nanoTime(); + } + + public CompletableFuture 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 store = new Object2ObjectLinkedOpenHashMap<>(); + + public Cache(Map> 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 get(Object key) { + if (key instanceof ResourceLocation rl) { + var entry = this.store.getAndMoveToLast(rl); + CompletableFuture result = entry != null ? entry.getBuffer() : null; + this.checkExpired(); + return result; + } else { + return null; + } + } + + @Override + public CompletableFuture put(ResourceLocation key, CompletableFuture 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>> entrySet() { + return new EntrySet(); + } + + private class EntrySet extends AbstractSet>> { + @Override + public Iterator>> iterator() { + var storeIter = store.entrySet().iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return storeIter.hasNext(); + } + + @Override + public Map.Entry> 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(); + } + } + } } diff --git a/common/src/main/java/org/embeddedt/modernfix/sound/SoundBufferCache.java b/common/src/main/java/org/embeddedt/modernfix/sound/SoundBufferCache.java new file mode 100644 index 00000000..36a10300 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/sound/SoundBufferCache.java @@ -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> { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return super.removeEldestEntry(eldest); + } +}