Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update redstone wire optimizations and fix #581 #583

Closed
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
@@ -0,0 +1,222 @@
package net.caffeinemc.mods.lithium.common.block.redstone;

import net.caffeinemc.mods.lithium.common.util.DirectionConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.redstone.RedstoneWireEvaluator;

/**
* Optimizing redstone dust is tricky, but even more so if you wish to preserve behavior
* perfectly. There are two big reasons redstone wire is laggy:
* <br>
* - It updates recursively. Each wire updates its power level in isolation, rather than
* in the context of the network it is a part of. This means each wire in a network
* could check and update its power level over half a dozen times. This also leads to
* way more shape and block updates than necessary.
* <br>
* - It emits copious amounts of duplicate and redundant shape and block updates. While
* the recursive updates are largely to blame, even a single state change leads to 18
* redundant block updates and up to 16 redundant shape updates.
*
* <p>
* Unfortunately fixing either of these aspects can be detected in-game, even if it is
* through obscure mechanics. Removing redundant block updates can be detected with
* something as simple as a redstone wire on a trapdoor, while removing the recursive
* updates can be detected with locational setups that rely on a specific block update
* order.
*
* <p>
* What we can optimize, however, are the power calculations. In vanilla, these are split
* into two parts:
* <br>
* - Power from non-wire components.
* <br>
* - Power from other redstone wires.
*
* <p>
* For the default evaluator, we can combine the two to reduce calls to Level.getBlockState
* and BlockState.isRedstoneConductor as well as calls to BlockState.getSignal and
* BlockState.getDirectSignal. We can avoid calling those last two methods on redstone wires
* altogether, since we know they should return 0.
* <br>
* These changes can lead to a mspt reduction of up to 20% on top of Lithium's other
* performance improvements.
*
* <p>
* The experimental evaluator actually uses the wire-power and non-wire-power separately,
* so we can't combine them. We can, however, optimize each individually, with the same
* principles.
*
* @author Space Walker
*/
public class RedstoneWirePowerCalculations {

private static final int MIN = 0; // smallest possible power value
private static final int MAX = 15; // largest possible power value
private static final int MAX_WIRE = MAX - 1; // largest possible power a wire can receive from another wire

private static Block wireBlock;
private static RedstoneWireEvaluator evaluator;
private static boolean ignoreWires = false;
private static boolean ignoreNonWires = false;

public static int getNeighborBlockSignal(Block wireBlock, RedstoneWireEvaluator evaluator, Level level, BlockPos pos) {
ignoreWires = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am concerned about these static fields, mod compatibility with threading mods will definitely suffer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think converting them to a local variable should be possible if I am not misunderstanding the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that'd work

int signal = getNeighborSignal(wireBlock, evaluator, level, pos);
ignoreWires = false;

return signal;
}

public static int getNeighborWireSignal(Block wireBlock, RedstoneWireEvaluator evaluator, Level level, BlockPos pos) {
ignoreNonWires = true;
int signal = getNeighborSignal(wireBlock, evaluator, level, pos);
ignoreNonWires = false;

return signal;
}

/**
* Calculate the redstone power a wire at the given location receives from the
* blocks around it.
*/
public static int getNeighborSignal(Block wireBlock, RedstoneWireEvaluator evaluator, Level level, BlockPos pos) {
RedstoneWirePowerCalculations.wireBlock = wireBlock;
RedstoneWirePowerCalculations.evaluator = evaluator;

int signal = MIN;
LevelChunk chunk = level.getChunkAt(pos);

if (!ignoreNonWires) {
for (Direction dir : DirectionConstants.VERTICAL) {
BlockPos side = pos.relative(dir);
BlockState neighbor = chunk.getBlockState(side);

// Wires do not accept power from other wires directly above or below them,
// so those can be ignored. Similarly, if there is air directly above or
// below a wire, it does not receive any power from that direction.
if (!neighbor.isAir() && !neighbor.is(wireBlock)) {
signal = Math.max(signal, getSignalFromVertical(level, side, neighbor, dir));

if (signal >= MAX) {
return MAX;
}
}
}
}

boolean checkWiresAbove = false;

if (!ignoreWires) {
// In vanilla this check is done up to 4 times.
BlockPos above = pos.above();
checkWiresAbove = !chunk.getBlockState(above).isRedstoneConductor(level, above);
}

for (Direction dir : DirectionConstants.HORIZONTAL) {
signal = Math.max(signal, getSignalFromSide(level, pos.relative(dir), dir, checkWiresAbove));

if (signal >= MAX) {
return MAX;
}
}

return signal;
}

/**
* Calculate the redstone power a wire receives from a block above or below it.
* We do these positions separately because there are no wire connections
* vertically. This simplifies the calculations a little.
*/
private static int getSignalFromVertical(Level level, BlockPos pos, BlockState state, Direction toDir) {
int signal = state.getSignal(level, pos, toDir);

if (signal >= MAX) {
return MAX;
}

if (state.isRedstoneConductor(level, pos)) {
return Math.max(signal, getDirectSignalTo(level, pos, toDir.getOpposite()));
}

return signal;
}

/**
* Calculate the redstone power a wire receives from blocks next to it.
*/
private static int getSignalFromSide(Level level, BlockPos pos, Direction toDir, boolean checkWiresAbove) {
LevelChunk chunk = level.getChunkAt(pos);
BlockState state = chunk.getBlockState(pos);

if (state.is(wireBlock)) {
return ignoreWires ? MIN : evaluator.getWireSignal(pos, state) - 1;
}

int signal = MIN;

if (!ignoreNonWires) {
signal = state.getSignal(level, pos, toDir);

if (signal >= MAX) {
return MAX;
}
}

if (state.isRedstoneConductor(level, pos)) {
if (!ignoreNonWires) {
signal = Math.max(signal, getDirectSignalTo(level, pos, toDir.getOpposite()));

if (signal >= MAX) {
return MAX;
}
}
if (!ignoreWires && checkWiresAbove && signal < MAX_WIRE) {
BlockPos above = pos.above();
BlockState aboveState = chunk.getBlockState(above);

if (aboveState.is(wireBlock)) {
signal = Math.max(signal, evaluator.getWireSignal(above, aboveState) - 1);
}
}
} else if (!ignoreWires && signal < MAX_WIRE) {
BlockPos below = pos.below();
BlockState belowState = chunk.getBlockState(below);

if (belowState.is(wireBlock)) {
signal = Math.max(signal, evaluator.getWireSignal(below, belowState) - 1);
}
}

return signal;
}

/**
* Calculate the strong power a block receives from the blocks around it.
*/
private static int getDirectSignalTo(Level level, BlockPos pos, Direction ignore) {
int signal = MIN;

for (Direction dir : DirectionConstants.ALL) {
if (dir != ignore) {
BlockPos side = pos.relative(dir);
BlockState neighbor = level.getBlockState(side);

if (!neighbor.isAir() && !neighbor.is(wireBlock)) {
signal = Math.max(signal, neighbor.getDirectSignal(level, side, dir));

if (signal >= MAX) {
return MAX;
}
}
}
}

return signal;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.caffeinemc.mods.lithium.mixin.block.redstone_wire;

import net.caffeinemc.mods.lithium.common.block.redstone.RedstoneWirePowerCalculations;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.RedStoneWireBlock;
import net.minecraft.world.level.redstone.DefaultRedstoneWireEvaluator;
import net.minecraft.world.level.redstone.RedstoneWireEvaluator;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(DefaultRedstoneWireEvaluator.class)
public abstract class DefaultRedstoneWireEvaluatorMixin extends RedstoneWireEvaluator {

private DefaultRedstoneWireEvaluatorMixin(RedStoneWireBlock wireBlock) {
super(wireBlock);
}

@Inject(
method = "calculateTargetStrength(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)I",
cancellable = true,
at = @At(
value = "HEAD"
)
)
private void calculateTargetStrengthFaster(Level level, BlockPos pos, CallbackInfoReturnable<Integer> cir) {
cir.setReturnValue(RedstoneWirePowerCalculations.getNeighborSignal(this.wireBlock, this, level, pos));
}
}
Loading