Toggleable update limiting

- Extract update limiting behavior to interface
 - Move original impl to BandedPrimeLimiter
 - Add dummy NonLimiter impl
 - Add command/config to toggle update limiting
 - Refactor InstanceManager to be more consistent between frame updates and tick updates
 - Bump version - 0.6.1
This commit is contained in:
Jozufozu 2022-02-01 13:54:38 -08:00
parent b403ca3d2b
commit 9219fef20a
13 changed files with 162 additions and 54 deletions

View file

@ -2,7 +2,7 @@ org.gradle.jvmargs = -Xmx3G
org.gradle.daemon = false
# mod version info
mod_version = 0.6.0
mod_version = 0.6.1
mc_update_version = 1.18
minecraft_version = 1.18.1
forge_version = 39.0.59

View file

@ -14,13 +14,14 @@ import com.jozufozu.flywheel.api.instance.DynamicInstance;
import com.jozufozu.flywheel.api.instance.TickableInstance;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.instancing.instancing.InstancingEngine;
import com.jozufozu.flywheel.backend.instancing.ratelimit.DistanceUpdateLimiter;
import com.jozufozu.flywheel.config.FlwConfig;
import com.jozufozu.flywheel.light.LightUpdater;
import com.mojang.math.Vector3f;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.Camera;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
public abstract class InstanceManager<T> implements InstancingEngine.OriginShiftListener {
@ -33,8 +34,8 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
protected final Object2ObjectOpenHashMap<T, TickableInstance> tickableInstances;
protected final Object2ObjectOpenHashMap<T, DynamicInstance> dynamicInstances;
protected int frame;
protected int tick;
protected DistanceUpdateLimiter frame;
protected DistanceUpdateLimiter tick;
public InstanceManager(MaterialManager materialManager) {
this.materialManager = materialManager;
@ -44,6 +45,10 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
this.dynamicInstances = new Object2ObjectOpenHashMap<>();
this.tickableInstances = new Object2ObjectOpenHashMap<>();
FlwConfig config = FlwConfig.get();
frame = config.createUpdateLimiter();
tick = config.createUpdateLimiter();
}
/**
@ -86,7 +91,7 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
* </p>
*/
public void tick(TaskEngine taskEngine, double cameraX, double cameraY, double cameraZ) {
tick++;
tick.tick();
processQueuedUpdates();
// integer camera pos as a micro-optimization
@ -112,7 +117,7 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
}
}
private void tickInstance(int cX, int cY, int cZ, TickableInstance instance) {
protected void tickInstance(int cX, int cY, int cZ, TickableInstance instance) {
if (!instance.decreaseTickRateWithDistance()) {
instance.tick();
return;
@ -124,11 +129,11 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
int dY = pos.getY() - cY;
int dZ = pos.getZ() - cZ;
if ((tick % getUpdateDivisor(dX, dY, dZ)) == 0) instance.tick();
if (tick.shouldUpdate(dX, dY, dZ)) instance.tick();
}
public void beginFrame(TaskEngine taskEngine, Camera info) {
frame++;
frame.tick();
processQueuedAdditions();
Vector3f look = info.getLookVector();
@ -151,8 +156,7 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
List<DynamicInstance> sub = instances.subList(start, end);
taskEngine.submit(() -> {
for (DynamicInstance dyn : sub) {
if (!dyn.decreaseFramerateWithDistance() || shouldFrameUpdate(dyn.getWorldPosition(), lookX, lookY, lookZ, cX, cY, cZ))
dyn.beginFrame();
updateInstance(dyn, lookX, lookY, lookZ, cX, cY, cZ);
}
});
@ -160,6 +164,28 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
}
}
protected void updateInstance(DynamicInstance dyn, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) {
if (!dyn.decreaseFramerateWithDistance()) {
dyn.beginFrame();
return;
}
BlockPos worldPos = dyn.getWorldPosition();
int dX = worldPos.getX() - cX;
int dY = worldPos.getY() - cY;
int dZ = worldPos.getZ() - cZ;
// is it more than 2 blocks behind the camera?
int dist = 2;
float dot = (dX + lookX * dist) * lookX + (dY + lookY * dist) * lookY + (dZ + lookZ * dist) * lookZ;
if (dot < 0) {
return;
}
if (frame.shouldUpdate(dX, dY, dZ))
dyn.beginFrame();
}
public void add(T obj) {
if (!Backend.isOn()) return;
@ -268,29 +294,6 @@ public abstract class InstanceManager<T> implements InstancingEngine.OriginShift
}
}
protected boolean shouldFrameUpdate(BlockPos worldPos, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) {
int dX = worldPos.getX() - cX;
int dY = worldPos.getY() - cY;
int dZ = worldPos.getZ() - cZ;
// is it more than 2 blocks behind the camera?
int dist = 2;
float dot = (dX + lookX * dist) * lookX + (dY + lookY * dist) * lookY + (dZ + lookZ * dist) * lookZ;
if (dot < 0) return false;
return (frame % getUpdateDivisor(dX, dY, dZ)) == 0;
}
// 1 followed by the prime numbers
private static final int[] divisorSequence = new int[] { 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
protected int getUpdateDivisor(int dX, int dY, int dZ) {
int dSq = dX * dX + dY * dY + dZ * dZ;
int i = (dSq / 2048);
return divisorSequence[Mth.clamp(i, 0, divisorSequence.length - 1)];
}
protected void addInternal(T obj) {
if (!Backend.isOn()) return;

View file

@ -4,6 +4,8 @@ import java.util.List;
import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.config.BooleanConfig;
import com.jozufozu.flywheel.config.FlwConfig;
import com.jozufozu.flywheel.event.BeginFrameEvent;
import com.jozufozu.flywheel.event.ReloadRenderersEvent;
import com.jozufozu.flywheel.event.RenderLayerEvent;
@ -123,6 +125,7 @@ public class InstancedRenderDispatcher {
if (Backend.isOn()) {
InstanceWorld instanceWorld = instanceWorlds.get(Minecraft.getInstance().level);
debug.add("Update limiting: " + BooleanConfig.boolToText(FlwConfig.get().limitUpdates()).getString());
debug.add("B: " + instanceWorld.blockEntityInstanceManager.getObjectCount() + ", E: " + instanceWorld.entityInstanceManager.getObjectCount());
instanceWorld.engine.addDebugInfo(debug);
} else {

View file

@ -154,9 +154,9 @@ public class InstancingEngine<P extends WorldProgram> implements Engine {
@Override
public void addDebugInfo(List<String> info) {
info.add("GL33 Instanced Arrays");
info.add("Origin: " + originCoordinate.getX() + ", " + originCoordinate.getY() + ", " + originCoordinate.getZ());
info.add("Instances: " + getGroupsToRender(null).mapToInt(InstancedMaterialGroup::getInstanceCount).sum());
info.add("Vertices: " + getGroupsToRender(null).mapToInt(InstancedMaterialGroup::getVertexCount).sum());
info.add("Origin: " + originCoordinate.getX() + ", " + originCoordinate.getY() + ", " + originCoordinate.getZ());
}
@FunctionalInterface

View file

@ -0,0 +1,28 @@
package com.jozufozu.flywheel.backend.instancing.ratelimit;
import net.minecraft.util.Mth;
public class BandedPrimeLimiter implements DistanceUpdateLimiter {
// 1 followed by the prime numbers
private static final int[] divisorSequence = new int[] { 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
private int tickCount = 0;
@Override
public void tick() {
tickCount++;
}
@Override
public boolean shouldUpdate(int dX, int dY, int dZ) {
return (tickCount % getUpdateDivisor(dX, dY, dZ)) == 0;
}
protected int getUpdateDivisor(int dX, int dY, int dZ) {
int dSq = dX * dX + dY * dY + dZ * dZ;
int i = (dSq / 2048);
return divisorSequence[Mth.clamp(i, 0, divisorSequence.length - 1)];
}
}

View file

@ -0,0 +1,20 @@
package com.jozufozu.flywheel.backend.instancing.ratelimit;
/**
* Interface for rate-limiting updates based on an object's distance from the camera.
*/
public interface DistanceUpdateLimiter {
/**
* Call this before every update.
*/
void tick();
/**
* Check to see if an object at the given position relative to the camera should be updated.
* @param dX The X distance from the camera.
* @param dY The Y distance from the camera.
* @param dZ The Z distance from the camera.
* @return {@code true} if the object should be updated, {@code false} otherwise.
*/
boolean shouldUpdate(int dX, int dY, int dZ);
}

View file

@ -0,0 +1,13 @@
package com.jozufozu.flywheel.backend.instancing.ratelimit;
public class NonLimiter implements DistanceUpdateLimiter {
@Override
public void tick() {
// noop
}
@Override
public boolean shouldUpdate(int dX, int dY, int dZ) {
return true;
}
}

View file

@ -1,7 +1,8 @@
package com.jozufozu.flywheel.config;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.jozufozu.flywheel.backend.Backend;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
@ -9,20 +10,36 @@ import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.TextComponent;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
public enum BooleanConfig {
NORMAL_OVERLAY(() -> BooleanConfig::normalOverlay),
;
NORMAL_OVERLAY(BooleanConfig::normalOverlay),
LIMIT_UPDATES(BooleanConfig::limitUpdates);
final Supplier<Consumer<BooleanDirective>> receiver;
final Consumer<BooleanDirective> receiver;
BooleanConfig(Supplier<Consumer<BooleanDirective>> receiver) {
BooleanConfig(Consumer<BooleanDirective> receiver) {
this.receiver = receiver;
}
@OnlyIn(Dist.CLIENT)
private static void limitUpdates(BooleanDirective booleanDirective) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null || booleanDirective == null) return;
if (booleanDirective == BooleanDirective.DISPLAY) {
Component text = new TextComponent("Update limiting is currently: ").append(boolToText(FlwConfig.get().limitUpdates()));
player.displayClientMessage(text, false);
return;
}
FlwConfig.get().client.limitUpdates.set(booleanDirective.get());
Component text = boolToText(FlwConfig.get().limitUpdates()).append(new TextComponent(" update limiting.").withStyle(ChatFormatting.WHITE));
player.displayClientMessage(text, false);
Backend.reloadWorldRenderers();
}
private static void normalOverlay(BooleanDirective state) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null || state == null) return;
@ -40,7 +57,7 @@ public enum BooleanConfig {
player.displayClientMessage(text, false);
}
private static MutableComponent boolToText(boolean b) {
public static MutableComponent boolToText(boolean b) {
return b ? new TextComponent("enabled").withStyle(ChatFormatting.DARK_GREEN) : new TextComponent("disabled").withStyle(ChatFormatting.RED);
}
}

View file

@ -20,17 +20,17 @@ public class BooleanConfigCommand {
public ArgumentBuilder<CommandSourceStack, ?> register() {
return Commands.literal(name)
.executes(context -> {
value.receiver.get().accept(BooleanDirective.DISPLAY);
value.receiver.accept(BooleanDirective.DISPLAY);
return Command.SINGLE_SUCCESS;
})
.then(Commands.literal("on")
.executes(context -> {
value.receiver.get().accept(BooleanDirective.TRUE);
value.receiver.accept(BooleanDirective.TRUE);
return Command.SINGLE_SUCCESS;
}))
.then(Commands.literal("off")
.executes(context -> {
value.receiver.get().accept(BooleanDirective.FALSE);
value.receiver.accept(BooleanDirective.FALSE);
return Command.SINGLE_SUCCESS;
}));
}

View file

@ -13,15 +13,20 @@ public class FlwCommands {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
dispatcher.register(Commands.literal("flywheel")
.then(debugCommand())
.then(debugNormalsCommand())
.then(backendCommand())
.then(limitUpdatesCommand())
);
}
private static ArgumentBuilder<CommandSourceStack, ?> debugCommand() {
private static ArgumentBuilder<CommandSourceStack, ?> debugNormalsCommand() {
return new BooleanConfigCommand("debugNormals", BooleanConfig.NORMAL_OVERLAY).register();
}
private static ArgumentBuilder<CommandSourceStack, ?> limitUpdatesCommand() {
return new BooleanConfigCommand("limitUpdates", BooleanConfig.LIMIT_UPDATES).register();
}
private static ArgumentBuilder<CommandSourceStack, ?> backendCommand() {
return Commands.literal("backend")
.executes(context -> {

View file

@ -2,6 +2,10 @@ package com.jozufozu.flywheel.config;
import org.apache.commons.lang3.tuple.Pair;
import com.jozufozu.flywheel.backend.instancing.ratelimit.BandedPrimeLimiter;
import com.jozufozu.flywheel.backend.instancing.ratelimit.DistanceUpdateLimiter;
import com.jozufozu.flywheel.backend.instancing.ratelimit.NonLimiter;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.common.ForgeConfigSpec.BooleanValue;
import net.minecraftforge.fml.ModLoadingContext;
@ -34,12 +38,25 @@ public class FlwConfig {
return client.debugNormals.get();
}
public boolean limitUpdates() {
return client.limitUpdates.get();
}
public DistanceUpdateLimiter createUpdateLimiter() {
if (limitUpdates()) {
return new BandedPrimeLimiter();
} else {
return new NonLimiter();
}
}
public static void init() {
}
public static class ClientConfig {
public final ForgeConfigSpec.EnumValue<FlwEngine> engine;
public final BooleanValue debugNormals;
public final BooleanValue limitUpdates;
public ClientConfig(ForgeConfigSpec.Builder builder) {
@ -48,6 +65,9 @@ public class FlwConfig {
debugNormals = builder.comment("Enable or disable a debug overlay that colors pixels by their normal")
.define("debugNormals", false);
limitUpdates = builder.comment("Enable or disable instance update limiting with distance.")
.define("limitUpdates", true);
}
}
}

View file

@ -1,10 +1,9 @@
package com.jozufozu.flywheel.core.crumbling;
import com.jozufozu.flywheel.api.MaterialManager;
import com.jozufozu.flywheel.api.instance.DynamicInstance;
import com.jozufozu.flywheel.backend.instancing.blockentity.BlockEntityInstanceManager;
import net.minecraft.core.BlockPos;
public class CrumblingInstanceManager extends BlockEntityInstanceManager {
public CrumblingInstanceManager(MaterialManager materialManager) {
@ -12,7 +11,7 @@ public class CrumblingInstanceManager extends BlockEntityInstanceManager {
}
@Override
protected boolean shouldFrameUpdate(BlockPos worldPos, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) {
return true;
protected void updateInstance(DynamicInstance dyn, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) {
dyn.beginFrame();
}
}

View file

@ -21,7 +21,7 @@ public class ForgeEvents {
if (Minecraft.getInstance().options.renderDebug) {
InstancedRenderDispatcher.getDebugString(event.getLeft());
InstancedRenderDispatcher.getDebugString(event.getRight());
}
}