diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java index 2336453d2..dfa9cba58 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java @@ -7,6 +7,8 @@ public final class DebugCounters { public static final LongAdder TRANSFER_WAIT_NANOS = new LongAdder(); public static final LongAdder PRESENTS = new LongAdder(); public static final LongAdder EMPTY_ACQUIRES = new LongAdder(); + public static final LongAdder PBR_ANIM_UPDATES = new LongAdder(); + public static final LongAdder PBR_ANIM_DRY_RUNS = new LongAdder(); private DebugCounters() {} } diff --git a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java index 000037626..ea436eea7 100644 --- a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java +++ b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java @@ -138,6 +138,8 @@ public final class FlybyRunner { private long transferWaitStartNs; private long presentsStart; private long emptyAcquiresStart; + private long pbrAnimUpdatesStart; + private long pbrAnimDryRunsStart; private long allocClientStart; private long allocProcessStart; private int frameCount; @@ -557,6 +559,8 @@ public final class FlybyRunner { this.transferWaitStartNs = DebugCounters.TRANSFER_WAIT_NANOS.sum(); this.presentsStart = DebugCounters.PRESENTS.sum(); this.emptyAcquiresStart = DebugCounters.EMPTY_ACQUIRES.sum(); + this.pbrAnimUpdatesStart = DebugCounters.PBR_ANIM_UPDATES.sum(); + this.pbrAnimDryRunsStart = DebugCounters.PBR_ANIM_DRY_RUNS.sum(); this.allocClientStart = AllocBean.allocatedBytes(); this.allocProcessStart = AllocBean.totalAllocatedBytes(); this.frameCount = 0; @@ -944,6 +948,9 @@ public final class FlybyRunner { sb.append(String.format(", presents %d, empty acquires %d", DebugCounters.PRESENTS.sum() - this.presentsStart, DebugCounters.EMPTY_ACQUIRES.sum() - this.emptyAcquiresStart)); + sb.append(String.format(", pbr anim updates %d, dry runs %d", + DebugCounters.PBR_ANIM_UPDATES.sum() - this.pbrAnimUpdatesStart, + DebugCounters.PBR_ANIM_DRY_RUNS.sum() - this.pbrAnimDryRunsStart)); } if (this.framesSeen > this.frameCount) sb.append(" (frame samples truncated)"); for (int i = 0; i < this.phaseCount; i++) { diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java index ea12ab2ba..0921d930b 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/interfaces/IPatchedTextureAtlasSprite.java @@ -5,5 +5,5 @@ public interface IPatchedTextureAtlasSprite { void angelica$markNeedsAnimationUpdate(); boolean angelica$needsAnimationUpdate(); void angelica$unmarkNeedsAnimationUpdate(); - void angelica$updateAnimationsDryRun(); + boolean angelica$tickAnimation(boolean onlyVisible); } diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java index a8c8cc2b7..c118fadc6 100644 --- a/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRAtlasTexture.java @@ -2,10 +2,15 @@ package net.coderbot.iris.texture.pbr; import com.gtnewhorizons.angelica.compat.mojang.AutoClosableAbstractTexture; import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import com.gtnewhorizons.angelica.proxy.ClientProxy; import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension; import com.gtnewhorizons.angelica.utils.SpritePadding; import lombok.Getter; import net.coderbot.iris.Iris; +import net.coderbot.iris.texture.pbr.loader.AtlasPBRLoader; import net.coderbot.iris.texture.util.TextureExporter; import net.coderbot.iris.texture.util.TextureManipulationUtil; import net.minecraft.client.renderer.texture.TextureAtlasSprite; @@ -20,10 +25,10 @@ import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; +import java.util.List; import java.util.Map; -import java.util.Set; public class PBRAtlasTexture extends AutoClosableAbstractTexture { protected final TextureMap texMap; @@ -31,7 +36,8 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture { protected final PBRType type; protected final ResourceLocation id; protected final Map sprites = new HashMap<>(); - protected final Set animatedSprites = new HashSet<>(); + protected final List animatedSprites = new ArrayList<>(); + protected final List alwaysAnimatedSprites = new ArrayList<>(); public PBRAtlasTexture(TextureMap textureMap, PBRType type) { this.texMap = textureMap; @@ -44,10 +50,15 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture { return id; } - public void addSprite(TextureAtlasSprite sprite) { + public void addSprite(AtlasPBRLoader.PBRTextureAtlasSprite sprite) { sprites.put(texMap.completeResourceLocation(new ResourceLocation(sprite.getIconName()), 0), sprite); - if (sprite.hasAnimationMetadata()) { - animatedSprites.add(sprite); + if (((ISpriteExt) sprite).isAnimation()) { + // Without the animation mixins, or for a base outside listAnimatedSprites, nothing consumes base visibility + if (sprite instanceof IPatchedTextureAtlasSprite patched && sprite.getBaseSprite().hasAnimationMetadata()) { + animatedSprites.add(patched); + } else { + alwaysAnimatedSprites.add(sprite); + } } } @@ -59,6 +70,7 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture { public void clear() { sprites.clear(); animatedSprites.clear(); + alwaysAnimatedSprites.clear(); } public void upload(int atlasWidth, int atlasHeight, int mipLevel, float anisotropicFiltering) { @@ -78,7 +90,7 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture { } } - if (!animatedSprites.isEmpty()) { + if (!animatedSprites.isEmpty() || !alwaysAnimatedSprites.isEmpty()) { final PBRAtlasHolder pbrHolder = ((TextureAtlasExtension) texMap).getOrCreatePBRHolder(); switch (type) { case NORMAL: @@ -128,10 +140,17 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture { } public void cycleAnimationFrames() { + final boolean onlyVisible = ClientProxy.options().performance.animateOnlyVisibleTextures; bind(); - for (TextureAtlasSprite sprite : animatedSprites) { - sprite.updateAnimation(); + int updated = alwaysAnimatedSprites.size(); + for (int i = 0; i < animatedSprites.size(); i++) { + if (animatedSprites.get(i).angelica$tickAnimation(onlyVisible)) updated++; } + for (int i = 0; i < alwaysAnimatedSprites.size(); i++) { + alwaysAnimatedSprites.get(i).updateAnimation(); + } + DebugCounters.PBR_ANIM_UPDATES.add(updated); + DebugCounters.PBR_ANIM_DRY_RUNS.add(animatedSprites.size() + alwaysAnimatedSprites.size() - updated); } @Override diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java index 89e818d8d..1f4b17283 100644 --- a/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java @@ -58,8 +58,8 @@ public class AtlasPBRLoader implements PBRTextureLoader { PBRAtlasTexture specularAtlas = null; for (TextureAtlasSprite sprite : (Collection) texMap.mapUploadedSprites.values()) { if (!(sprite.getIconName().equals("missingno"))) { - TextureAtlasSprite normalSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.NORMAL); - TextureAtlasSprite specularSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.SPECULAR); + PBRTextureAtlasSprite normalSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.NORMAL); + PBRTextureAtlasSprite specularSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.SPECULAR); if (normalSprite != null) { if (normalAtlas == null) { normalAtlas = new PBRAtlasTexture(texMap, PBRType.NORMAL); @@ -115,7 +115,7 @@ public class AtlasPBRLoader implements PBRTextureLoader { } @Nullable - protected TextureAtlasSprite createPBRSprite(TextureAtlasSprite sprite, IResourceManager resourceManager, TextureMap texMap, int atlasWidth, int atlasHeight, int mipLevel, PBRType pbrType) { + protected PBRTextureAtlasSprite createPBRSprite(TextureAtlasSprite sprite, IResourceManager resourceManager, TextureMap texMap, int atlasWidth, int atlasHeight, int mipLevel, PBRType pbrType) { final ResourceLocation spriteName = new ResourceLocation(sprite.getIconName()); final ResourceLocation imageLocation = texMap.completeResourceLocation(spriteName, 0); final ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation); @@ -124,7 +124,7 @@ public class AtlasPBRLoader implements PBRTextureLoader { return null; } - TextureAtlasSprite pbrSprite = null; + PBRTextureAtlasSprite pbrSprite = null; try { // This is no longer closable. Not sure about this. @@ -174,7 +174,7 @@ public class AtlasPBRLoader implements PBRTextureLoader { final int gutter = ((SpriteExtension) sprite).angelica$getGutterWidth(); pbrSprite = new PBRTextureAtlasSprite(pbrSpriteInfo, animationMetadata, atlasWidth, atlasHeight, - sprite.getOriginX(), sprite.getOriginY(), gutter, nativeImage, false, mipLevel); + sprite.getOriginX(), sprite.getOriginY(), gutter, nativeImage, false, mipLevel, sprite); syncAnimation(sprite, pbrSprite); } catch (FileNotFoundException e) { // @@ -234,14 +234,21 @@ public class AtlasPBRLoader implements PBRTextureLoader { } public static class PBRTextureAtlasSprite extends TextureAtlasSprite implements CustomMipmapGenerator.Provider { + protected final TextureAtlasSprite baseSprite; + // This feels super janky - protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, int gutter, NativeImage nativeImage, boolean useAnisotropicFiltering, int miplevel) { + protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, int gutter, NativeImage nativeImage, boolean useAnisotropicFiltering, int miplevel, TextureAtlasSprite baseSprite) { super(info.name().toString()); + this.baseSprite = baseSprite; ((SpriteExtension) this).angelica$setGutterWidth(gutter); super.loadSprite(Objects.requireNonNull(getMipmapGenerator(info, atlasWidth, atlasHeight)).generateMipLevels(nativeImage, miplevel), animationMetaDataSection, useAnisotropicFiltering); super.initSprite(atlasWidth, atlasHeight, x - gutter, y - gutter, false); } + public TextureAtlasSprite getBaseSprite() { + return baseSprite; + } + @Override public CustomMipmapGenerator getMipmapGenerator(TextureAtlasSpriteInfo info, int atlasWidth, int atlasHeight) { if (info instanceof PBRTextureAtlasSpriteInfo pbrInfo) { diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java index ed6f020a4..2b580291e 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureAtlasSprite.java @@ -2,6 +2,8 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.animation; import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils; +import net.coderbot.iris.texture.pbr.PBRSpriteHolder; +import net.coderbot.iris.texture.pbr.TextureAtlasSpriteExtension; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.resources.data.AnimationMetadataSection; import org.spongepowered.asm.mixin.Mixin; @@ -18,6 +20,7 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr @Shadow protected int frameCounter; @Shadow private AnimationMetadataSection animationMetadata; @Shadow protected List framesTextureData; + @Shadow public abstract void updateAnimation(); @Unique private boolean angelica$isActive = false; @@ -31,6 +34,16 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr public boolean angelica$needsAnimationUpdate() { if (this.angelica$isActive) { this.angelica$isActive = false; + // PBR sprites are never UV-sampled; they inherit visibility when the base sprite's mark is consumed + if (this instanceof TextureAtlasSpriteExtension extension) { + final PBRSpriteHolder pbrHolder = extension.getPBRHolder(); + if (pbrHolder != null) { + final TextureAtlasSprite normalSprite = pbrHolder.getNormalSprite(); + final TextureAtlasSprite specularSprite = pbrHolder.getSpecularSprite(); + if (normalSprite != null) ((IPatchedTextureAtlasSprite) normalSprite).angelica$markNeedsAnimationUpdate(); + if (specularSprite != null) ((IPatchedTextureAtlasSprite) specularSprite).angelica$markNeedsAnimationUpdate(); + } + } return true; } return false; @@ -42,7 +55,17 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr } @Override - public void angelica$updateAnimationsDryRun() { + public boolean angelica$tickAnimation(boolean onlyVisible) { + if (!onlyVisible || angelica$needsAnimationUpdate()) { + updateAnimation(); + return true; + } + angelica$advanceFramesDryRun(); + return false; + } + + @Unique + private void angelica$advanceFramesDryRun() { if (animationMetadata == null || framesTextureData == null) return; tickCounter++; diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java index 23454ee1b..33ea1f0e1 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/animation/MixinTextureMap.java @@ -36,16 +36,7 @@ public abstract class MixinTextureMap extends AbstractTexture { final int size = listAnimatedSprites.size(); for (int i = 0; i < size; i++) { - final TextureAtlasSprite sprite = listAnimatedSprites.get(i); - final IPatchedTextureAtlasSprite patched = (IPatchedTextureAtlasSprite) sprite; - - // needsAnimationUpdate() is one-shot: returns true if marked, then auto-resets - if (!renderVisible || patched.angelica$needsAnimationUpdate()) { - sprite.updateAnimation(); - } else { - // Keep frame counters in sync for sprites not being updated - patched.angelica$updateAnimationsDryRun(); - } + ((IPatchedTextureAtlasSprite) listAnimatedSprites.get(i)).angelica$tickAnimation(renderVisible); } Minecraft.getMinecraft().mcProfiler.endSection(); } diff --git a/src/test/java/net/coderbot/iris/texture/pbr/PBRAtlasAnimationTest.java b/src/test/java/net/coderbot/iris/texture/pbr/PBRAtlasAnimationTest.java new file mode 100644 index 000000000..10d576193 --- /dev/null +++ b/src/test/java/net/coderbot/iris/texture/pbr/PBRAtlasAnimationTest.java @@ -0,0 +1,89 @@ +package net.coderbot.iris.texture.pbr; + +import com.gtnewhorizons.angelica.glsm.GLCoreTest; +import com.gtnewhorizons.angelica.glsm.GLSMTextures; +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite; +import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt; +import com.gtnewhorizons.angelica.proxy.ClientProxy; +import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions; +import net.coderbot.iris.texture.pbr.loader.AtlasPBRLoader; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureMap; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@GLCoreTest +class PBRAtlasAnimationTest { + static Stream cases() { + return Stream.of( + Arguments.of("visible tracked sprite updates", true, true, true, true, true, 1, 0, 1, 0), + Arguments.of("hidden tracked sprite dry-runs", true, true, true, true, false, 1, 0, 0, 1), + Arguments.of("only-visible off reaches the gate", true, true, true, false, true, 1, 0, 1, 0), + Arguments.of("base without animation metadata is always animated", true, false, true, true, false, 0, 1, 1, 0), + Arguments.of("sprite without the animation mixin is always animated", false, true, true, true, false, 0, 1, 1, 0), + Arguments.of("non-animated pbr sprite is never touched", true, true, false, true, false, 0, 0, 0, 0) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("cases") + void cycleRoutesAnimationAndCountsUpdates(String name, boolean patchedSprite, boolean baseAnimated, boolean pbrAnimated, + boolean onlyVisible, boolean tickReturns, int expectedTicks, int expectedUpdateCalls, + long expectedUpdates, long expectedDryRuns) { + final SodiumGameOptions previousOptions = Reflect.getStatic(ClientProxy.class, "CONFIG"); + final SodiumGameOptions options = new SodiumGameOptions(); + options.performance.animateOnlyVisibleTextures = onlyVisible; + + final TextureMap textureMap = mock(TextureMap.class, withSettings().extraInterfaces(TextureAtlasExtension.class)); + when(textureMap.completeResourceLocation(any(), anyInt())).thenAnswer(invocation -> invocation.getArgument(0)); + final TextureAtlasSprite base = mock(TextureAtlasSprite.class); + when(base.hasAnimationMetadata()).thenReturn(baseAnimated); + final AtlasPBRLoader.PBRTextureAtlasSprite sprite = mock(AtlasPBRLoader.PBRTextureAtlasSprite.class, patchedSprite + ? withSettings().extraInterfaces(IPatchedTextureAtlasSprite.class, ISpriteExt.class) + : withSettings().extraInterfaces(ISpriteExt.class)); + when(sprite.getIconName()).thenReturn("minecraft:stone_n"); + when(sprite.getBaseSprite()).thenReturn(base); + when(((ISpriteExt) sprite).isAnimation()).thenReturn(pbrAnimated); + if (patchedSprite) { + when(((IPatchedTextureAtlasSprite) sprite).angelica$tickAnimation(anyBoolean())).thenReturn(tickReturns); + } + + Reflect.setStatic(ClientProxy.class, "CONFIG", options); + final PBRAtlasTexture atlas = new PBRAtlasTexture(textureMap, PBRType.NORMAL); + final int texture = GLSMTextures.glGenTextures(); + Reflect.set(atlas, "glTextureId", texture); + try { + atlas.addSprite(sprite); + final long updatesBefore = DebugCounters.PBR_ANIM_UPDATES.sum(); + final long dryRunsBefore = DebugCounters.PBR_ANIM_DRY_RUNS.sum(); + + atlas.cycleAnimationFrames(); + + if (patchedSprite) { + verify((IPatchedTextureAtlasSprite) sprite, times(expectedTicks)).angelica$tickAnimation(anyBoolean()); + verify((IPatchedTextureAtlasSprite) sprite, times(expectedTicks)).angelica$tickAnimation(onlyVisible); + } + verify(sprite, times(expectedUpdateCalls)).updateAnimation(); + assertEquals(expectedUpdates, DebugCounters.PBR_ANIM_UPDATES.sum() - updatesBefore); + assertEquals(expectedDryRuns, DebugCounters.PBR_ANIM_DRY_RUNS.sum() - dryRunsBefore); + } finally { + GLSMTextures.glDeleteTextures(texture); + Reflect.setStatic(ClientProxy.class, "CONFIG", previousOptions); + } + } +}