From 273bab7856adb0b2dedfe9ce8f97f3c760669dd8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:52:56 -0400 Subject: [PATCH 1/2] Rewrite dynamic_sounds to consider sound duration when caching Related: #594 --- .../SoundBufferLibraryMixin.java | 32 +--- .../perf/dynamic_sounds/SoundBufferMixin.java | 37 +++++ .../dynamicresources/DynamicSoundHelpers.java | 142 +++++++++++++++++- .../modernfix/sound/SoundBufferCache.java | 15 ++ 4 files changed, 197 insertions(+), 29 deletions(-) create mode 100644 common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_sounds/SoundBufferMixin.java create mode 100644 common/src/main/java/org/embeddedt/modernfix/sound/SoundBufferCache.java 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); + } +} From fd46baae21e3104d5b730fcc394f27c712d02a5c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:18:17 -0400 Subject: [PATCH 2/2] Backport model compat fix from 1.21 https://github.com/embeddedt/ModernFix/commit/ff6b687d5af897c9cc494cc24c4761dcfba3e84a Related: #606 --- .../dynamic_resources/ModelManagerMixin.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java index 2a1ce44b..82e62917 100644 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java @@ -1,11 +1,14 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.ModelManager; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.GsonHelper; import net.minecraft.world.level.block.Block; @@ -19,6 +22,7 @@ import org.spongepowered.asm.mixin.Mixin; 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.ModifyArg; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -28,8 +32,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.function.Function; import java.util.stream.Collectors; @Mixin(ModelManager.class) @@ -44,10 +51,12 @@ public class ModelManagerMixin { } } - @Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockModels(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) - private CompletableFuture> deferBlockModelLoad(ResourceManager manager, Executor executor) { - var cache = CacheUtil.simpleCacheForLambda(location -> loadSingleBlockModel(manager, location), 100L); - return CompletableFuture.completedFuture(new LambdaMap<>(location -> cache.getUnchecked(location))); + @ModifyArg(method = "loadBlockModels", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0), index = 0) + private static Function, ? extends CompletionStage>> deferBlockModelLoad(Function, ? extends CompletionStage>> fn, @Local(ordinal = 0, argsOnly = true) ResourceManager manager) { + return resourceMap -> { + var cache = CacheUtil.simpleCacheForLambda(location -> loadSingleBlockModel(manager, location), 100L); + return CompletableFuture.completedFuture(Maps.asMap(Set.copyOf(resourceMap.keySet()), location -> cache.getUnchecked(location))); + }; } @Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockStates(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) @@ -61,7 +70,7 @@ public class ModelManagerMixin { return ImmutableList.of(); } - private BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location) { + private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location) { return manager.getResource(location).map(resource -> { try (BufferedReader reader = resource.openAsReader()) { return BlockModel.fromStream(reader);