Only animate PBR textures that are visible (#2310)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-28 20:46:51 -07:00
1 parent ed6dd12db7
commit 08aa37712f
8 files changed
+165 -27

No files matched your search

@@ -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() {}
}
@@ -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++) {
@@ -5,5 +5,5 @@ public interface IPatchedTextureAtlasSprite {
void angelica$markNeedsAnimationUpdate();
boolean angelica$needsAnimationUpdate();
void angelica$unmarkNeedsAnimationUpdate();
void angelica$updateAnimationsDryRun();
boolean angelica$tickAnimation(boolean onlyVisible);
}
@@ -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<ResourceLocation, TextureAtlasSprite> sprites = new HashMap<>();
protected final Set<TextureAtlasSprite> animatedSprites = new HashSet<>();
protected final List<IPatchedTextureAtlasSprite> animatedSprites = new ArrayList<>();
protected final List<TextureAtlasSprite> 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
@@ -58,8 +58,8 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
PBRAtlasTexture specularAtlas = null;
for (TextureAtlasSprite sprite : (Collection<TextureAtlasSprite>) 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<TextureMap> {
}
@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<TextureMap> {
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<TextureMap> {
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<TextureMap> {
}
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) {
@@ -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++;
@@ -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();
}
@@ -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<Arguments> 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);
}
}
}