Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.AABB;

import java.util.concurrent.atomic.AtomicInteger;

public interface BlockEntityCullBox {
AtomicInteger RENDERER_EPOCH = new AtomicInteger(1);

ClassValue<Boolean> FIXED_BOUNDS = new ClassValue<>() {
@Override
protected Boolean computeValue(Class<?> renderer) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.polyfrost.polydium.client.perf;

/**
* Entity Culling's filtered block entity list for one chunk, valid while the chunk's block entity map and the
* generation (renderer reloads, whitelist edits) are unchanged.
*/
public interface ChunkPrefetchCache {
Object[] polydium$storedPrefetch();

int polydium$prefetchGeneration();

void polydium$setPrefetch(Object[] cullables, int generation);

default Object[] polydium$prefetch(int generation) {
return generation == this.polydium$prefetchGeneration() ? this.polydium$storedPrefetch() : null;
}

default void polydium$invalidatePrefetch() {
this.polydium$setPrefetch(null, 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.polyfrost.polydium.client.perf;

public interface UpscaledSprite {
int polydium$upscale();

void polydium$setUpscale(int factor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@
//? if >= 1.21.11 {
import oshi.SystemInfo;
import oshi.hardware.GraphicsCard;
import org.lwjgl.system.APIUtil;
import org.lwjgl.system.JNI;
import org.lwjgl.system.SharedLibrary;

import java.io.IOException;
import java.nio.ByteBuffer;

import static org.lwjgl.system.MemoryUtil.memAddress;
import static org.lwjgl.system.MemoryUtil.memFree;
import static org.lwjgl.system.MemoryUtil.memUTF16;
//?}

// Kept apart from EarlyVulkan: the JVM remembers a failed class lookup per calling class, and EarlyVulkan loads before oshi can
public final class EarlyVulkanPreLaunch implements PreLaunchEntrypoint {
@Override
public void onPreLaunch() {
//? if >= 1.21.11
showSwitchableGpus();
detect(/*? if >= 26.2 {*/true/*?} else {*//*false*//*?}*/);
}

Expand All @@ -33,4 +45,36 @@ private void detect(boolean decide) {
}
//?}
}

//? if >= 1.21.11 {
// NVIDIA's and AMD's implicit switchable-graphics Vulkan layers hide the dGPU from vkEnumeratePhysicalDevices
// when their driver profile picks the iGPU, even with the Windows GPU preference set to high performance.
// The loader reads these with GetEnvironmentVariable at vkCreateInstance, so setting them here is early enough.
// An explicit Windows "Power saving" choice for this executable is kept, since unhiding the dGPU would override it.
private static void showSwitchableGpus() {
if (!System.getProperty("os.name", "").startsWith("Windows"))
return;
try (SharedLibrary kernel32 = APIUtil.apiCreateLibrary("kernel32")) {
if (windowsPrefersPowerSaving())
return;
long setEnv = kernel32.getFunctionAddress("SetEnvironmentVariableW");
for (String name : new String[]{"DISABLE_LAYER_NV_OPTIMUS_1", "DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1"}) {
ByteBuffer key = memUTF16(name);
ByteBuffer value = memUTF16("1");
JNI.callPPI(memAddress(key), memAddress(value), setEnv);
memFree(key);
memFree(value);
}
} catch (Throwable ignored) {
}
}

private static boolean windowsPrefersPowerSaving() throws IOException {
String exe = ProcessHandle.current().info().command().orElse(null);
if (exe == null)
return false;
Process reg = new ProcessBuilder("reg", "query", "HKCU\\Software\\Microsoft\\DirectX\\UserGpuPreferences", "/v", exe).redirectErrorStream(true).start();
return new String(reg.getInputStream().readAllBytes()).contains("GpuPreference=1;");
}
//?}
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,13 @@ private static boolean standsDown(String mixinClassName) {
Map.entry("OcclusionCull", new String[]{"OcclusionRegion", "OcclusionListAccessor"}),
Map.entry("ImmediateDrawRing", new String[]{"ImmediateDrawRingSeparate", "ImmediateDrawRingEmulated", "ImmediateDrawRingFrame", "ImmediateDrawRingClose"}),
Map.entry("EntityCullingBlockEntityBox", new String[]{"MixinBlockEntityCullBox"}),
Map.entry("EntityCullingPrefetch", new String[]{"EntityCullingBlockEntityBox", "MixinBlockEntityCullBox", "EntityCullingPrefetchChunk"}),
Map.entry("ParticleLightBatch", new String[]{"ParticleLightDrain", "ParticleLightSourceInvoker", "ParticleLevelAccessor"}),
Map.entry("ParticleCoreAsyncThreshold", new String[]{"ParticleSoundClientThread"}),
Map.entry("PreparedFrameRecover", new String[]{"PreparedFrameAccessor", "StagedVertexBufferAccessor"}),
Map.entry("IrisShadowOutlineDrop", new String[]{"BufferSourceAccessor", "OutlineBufferSourceAccessor"}),
Map.entry("SpriteMipUpscale", new String[]{"SpriteContentsAccessor", "SpriteContentsUpscale"}),
Map.entry("ItemModelUpscale", new String[]{"SpriteContentsUpscale"}),
//? if < 1.21.10 {
/*Map.entry("OpaqueSpriteParticle", new String[]{"SpriteContentsTransparency", "OpaqueParticleRenderOrder"}),
*///?} else {
Expand Down Expand Up @@ -435,6 +438,12 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (mixinClassName.endsWith(".MixinLeafCullScratchPos") && !Mods.loaded("cull-fewer-leaves"))
return false;

if (mixinClassName.endsWith(".MixinExpandedCylindricalGate") && !Mods.loaded("sodium-extra"))
return false;

if (mixinClassName.endsWith(".MixinTierTaggerModeCache") && !Mods.loaded("tiertagger") && !Mods.loaded("tier-tagger"))
return false;

if ((mixinClassName.endsWith(".MixinClientLightBudget") || mixinClassName.endsWith(".MixinClientLightBudgetTick") || mixinClassName.endsWith(".MixinChunkLoadDirtyOnce")) && !Mods.loaded("scalablelux"))
return false;

Expand Down Expand Up @@ -463,7 +472,7 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (mixinClassName.endsWith(".MixinReplayEntityTickets") && !Mods.loaded("flashback"))
return false;

if ((mixinClassName.endsWith(".MixinEntityCullingBlockEntityBox") || mixinClassName.endsWith(".MixinBlockEntityCullBox")) && !Mods.loaded("entityculling"))
if ((mixinClassName.endsWith(".MixinEntityCullingBlockEntityBox") || mixinClassName.endsWith(".MixinBlockEntityCullBox") || mixinClassName.contains(".MixinEntityCullingPrefetch")) && !Mods.loaded("entityculling"))
return false;

if (mixinClassName.endsWith(".MixinBlockModelHookPrecheck") && !Mods.loaded("obe") && !Mods.loaded("overflowanimations"))
Expand Down Expand Up @@ -644,6 +653,13 @@ public void postApply(String targetClassName, ClassNode targetClass, String mixi
return;
}

if (mixinClassName.endsWith(".MixinExpandedCylindricalGate")) {
for (MethodNode method : targetClass.methods)
if (method.name.equals("processQueue"))
gateExpandedCylindricalTraversal(targetClass, method);
return;
}

if (mixinClassName.endsWith(".MixinClientLightBudget")) {
for (MethodNode method : targetClass.methods)
if (method.name.equals("propagateChanges") && method.desc.equals("()V"))
Expand Down Expand Up @@ -1488,6 +1504,56 @@ private static void chunkLoadDirtyOnce(ClassNode owner, MethodNode method) {
}
}

// Sodium Extra's @ModifyArgs on visitNeighbors boxes every argument into an Args object for each section the occlusion
// search visits, even though its handler returns untouched unless expanded cylindrical fog culling is active. Mixin
// emits `Args$N.of(args)`, the handler call, then Args unpacking right before the original call, with the same stack
// before `of` as at the call, so the handler's own early-out can jump straight to the plain call.
public static void gateExpandedCylindricalTraversal(ClassNode owner, MethodNode method) {
String fog = "me/flashyreese/mods/sodiumextra/client/fog/FogDistanceHelper";
for (AbstractInsnNode insn : method.instructions.toArray()) {
if (!(insn instanceof MethodInsnNode handler) || !handler.owner.equals(owner.name) || !handler.name.endsWith("allowExpandedCylindricalVerticalTraversal")
|| !handler.desc.equals("(Lorg/spongepowered/asm/mixin/injection/invoke/arg/Args;)V"))
continue;
AbstractInsnNode pack = handler.getPrevious();
while (pack instanceof InsnNode || pack instanceof VarInsnNode)
pack = pack.getPrevious();
if (!(pack instanceof MethodInsnNode of) || of.getOpcode() != Opcodes.INVOKESTATIC || !of.name.equals("of"))
continue;
AbstractInsnNode next = handler.getNext();
while (next instanceof InsnNode || next instanceof MethodInsnNode call && call.owner.equals(of.owner))
next = next.getNext();
if (!(next instanceof MethodInsnNode neighbors) || !neighbors.name.equals("visitNeighbors")
|| !Arrays.equals(Type.getArgumentTypes(neighbors.desc), Type.getArgumentTypes(of.desc)))
continue;

LabelNode plain = new LabelNode();
InsnList gate = new InsnList();
if (handler.getOpcode() == Opcodes.INVOKESTATIC) {
Type[] params = Type.getArgumentTypes(method.desc);
if ((method.access & Opcodes.ACC_STATIC) == 0 || params.length < 3 || !params[2].equals(Type.FLOAT_TYPE))
continue;
gate.add(new VarInsnNode(Opcodes.FLOAD, 2));
gate.add(new MethodInsnNode(Opcodes.INVOKESTATIC, fog, "isExpandedCylindricalCullDistance", "(F)Z", false));
gate.add(new JumpInsnNode(Opcodes.IFEQ, plain));
} else {
String[] distances = {"searchDistanceRegular", "searchDistanceLocal"};
if ((method.access & Opcodes.ACC_STATIC) != 0 || !Arrays.stream(distances).allMatch(name -> owner.fields.stream().anyMatch(field -> field.name.equals(name) && field.desc.equals("F"))))
continue;
LabelNode run = new LabelNode();
for (String name : distances) {
gate.add(new VarInsnNode(Opcodes.ALOAD, 0));
gate.add(new FieldInsnNode(Opcodes.GETFIELD, owner.name, name, "F"));
gate.add(new MethodInsnNode(Opcodes.INVOKESTATIC, fog, "isExpandedCylindricalCullDistance", "(F)Z", false));
gate.add(new JumpInsnNode(Opcodes.IFNE, run));
}
gate.add(new JumpInsnNode(Opcodes.GOTO, plain));
gate.add(run);
}
method.instructions.insertBefore(of, gate);
method.instructions.insertBefore(neighbors, plain);
}
}

private static void budgetLightTasks(ClassNode owner, MethodNode method) {
if (owner.fields.stream().noneMatch(field -> field.name.equals("isClientSide") && field.desc.equals("Z")))
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,18 @@ public abstract class MixinEntityCullingBlockEntityBox {
@Shadow
public abstract <E extends BlockEntity, S extends BlockEntityRenderState> BlockEntityRenderer<E, S> getRenderer(E blockEntity);

@Unique
private int polydium$rendererEpoch = 1;
@Unique
private BlockEntity polydium$cullTarget;

@Inject(method = "onResourceManagerReload", at = @At("RETURN"))
private void polydium$invalidateCullBoxes(CallbackInfo ci) {
this.polydium$rendererEpoch++;
BlockEntityCullBox.RENDERER_EPOCH.incrementAndGet();
}

@TargetHandler(mixin = POLYDIUM$MIXIN, name = "tryExtractRenderState")
@WrapOperation(method = "@MixinSquared:Handler", at = @At(value = "INVOKE", target = "Ldev/tr7zw/entityculling/EntityCullingModBase;setupAABB(Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/AABB;"), require = 0, expect = 0)
private AABB polydium$inline$cachedBox(@Coerce Object mod, BlockEntity blockEntity, BlockPos pos, Operation<AABB> original) {
AABB cached = ((BlockEntityCullBox) blockEntity).polydium$cullBox(this.polydium$rendererEpoch);
AABB cached = ((BlockEntityCullBox) blockEntity).polydium$cullBox(BlockEntityCullBox.RENDERER_EPOCH.get());
this.polydium$cullTarget = cached == null ? blockEntity : null;
return cached != null ? cached : original.call(mod, blockEntity, pos);
}
Expand All @@ -60,9 +58,10 @@ public abstract class MixinEntityCullingBlockEntityBox {
if (blockEntity != null) {
this.polydium$cullTarget = null;
BlockEntityCullBox holder = (BlockEntityCullBox) blockEntity;
if (holder.polydium$cullBoxEpoch() != this.polydium$rendererEpoch) {
int epoch = BlockEntityCullBox.RENDERER_EPOCH.get();
if (holder.polydium$cullBoxEpoch() != epoch) {
boolean fixed = BlockEntityCullBox.fixedBounds(this.getRenderer(blockEntity).getClass());
holder.polydium$setCullBox(fixed ? box : null, this.polydium$rendererEpoch);
holder.polydium$setCullBox(fixed ? box : null, epoch);
}
}
return frustum.isVisible(box);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.polyfrost.polydium.mixin.client.perf;

//? if >= 1.21.10 {
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.chunk.LevelChunk;
import org.polyfrost.polydium.client.perf.BlockEntityCullBox;
import org.polyfrost.polydium.client.perf.ChunkPrefetchCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

// Entity Culling rescans every block entity in 17x17 chunks each tick; reuse a chunk's filtered list until it changes
@Pseudo
@Mixin(targets = "dev.tr7zw.entityculling.EntityCullingModBase", remap = false)
public abstract class MixinEntityCullingPrefetch {
@Shadow
public Set<BlockEntityType<?>> blockEntityWhitelist;

@Unique
private int polydium$generation;
@Unique
private int polydium$rendererEpoch;
@Unique
private Set<BlockEntityType<?>> polydium$whitelist = Set.of();

@Inject(method = "extractEntityData", at = @At("HEAD"), require = 0, expect = 0)
private void polydium$checkPrefetchInputs(CallbackInfo ci) {
if (!Minecraft.getInstance().isSameThread())
return;
int epoch = BlockEntityCullBox.RENDERER_EPOCH.get();
// The config screen edits the whitelist in place
if (epoch != this.polydium$rendererEpoch || !this.polydium$whitelist.equals(this.blockEntityWhitelist)) {
this.polydium$rendererEpoch = epoch;
this.polydium$whitelist = Set.copyOf(this.blockEntityWhitelist);
this.polydium$generation++;
}
}

@WrapOperation(method = "extractEntityData", at = @At(value = "INVOKE", target = "Ldev/tr7zw/entityculling/EntityCullingModBase;prefetchBlockEntityData(Ljava/util/List;Ljava/util/Map;)V"), require = 0, expect = 0)
private void polydium$cachedPrefetch(@Coerce Object mod, List<Object> out, Map<BlockPos, BlockEntity> blockEntities, Operation<Void> original, @Local LevelChunk chunk) {
Minecraft client = Minecraft.getInstance();
// With safe mode off this runs on the cull thread, racing chunk updates
if (!client.isSameThread()) {
original.call(mod, out, blockEntities);
return;
}
ChunkPrefetchCache cache = (ChunkPrefetchCache) chunk;
Object[] cached = cache.polydium$prefetch(this.polydium$generation);
if (cached != null) {
for (Object cullable : cached)
out.add(cullable);
return;
}
int start = out.size();
original.call(mod, out, blockEntities);
List<Object> added = out.subList(start, out.size());
BlockEntityRenderDispatcher dispatcher = client.getBlockEntityRenderDispatcher();
// A renderer with its own getRenderBoundingBox may move its box every tick, so that chunk is rebuilt each time
for (Object cullable : added) {
if (!BlockEntityCullBox.fixedBounds(dispatcher.getRenderer((BlockEntity) cullable).getClass()))
return;
}
cache.polydium$setPrefetch(added.toArray(), this.polydium$generation);
}
}
//?}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.polyfrost.polydium.mixin.client.perf;

//? if >= 1.21.10 {
import net.minecraft.world.level.chunk.LevelChunk;
import org.polyfrost.polydium.client.perf.ChunkPrefetchCache;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

// These are the only LevelChunk methods that change the block entity map's contents on the client
@Mixin(LevelChunk.class)
public abstract class MixinEntityCullingPrefetchChunk implements ChunkPrefetchCache {
@Unique
private Object[] polydium$prefetch;
@Unique
private int polydium$prefetchGeneration;

@Override
public Object[] polydium$storedPrefetch() {
return this.polydium$prefetch;
}

@Override
public int polydium$prefetchGeneration() {
return this.polydium$prefetchGeneration;
}

@Override
public void polydium$setPrefetch(Object[] cullables, int generation) {
this.polydium$prefetch = cullables;
this.polydium$prefetchGeneration = generation;
}

@Inject(method = {"setBlockEntity", "removeBlockEntity", "clearAllBlockEntities"}, at = @At("HEAD"))
private void polydium$invalidateOnChange(CallbackInfo ci) {
this.polydium$invalidatePrefetch();
}

@Inject(method = "getBlockEntity(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/chunk/LevelChunk$EntityCreationType;)Lnet/minecraft/world/level/block/entity/BlockEntity;", at = @At(value = "INVOKE", target = "Ljava/util/Map;remove(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 1))
private void polydium$invalidateOnDrop(CallbackInfoReturnable<?> cir) {
this.polydium$invalidatePrefetch();
}
}
//?}
Loading
Loading