misc bounds benchmark, property extractor, scaffolder

This commit is contained in:
Leijurv
2021-05-24 17:16:25 -07:00
parent a9b7b91a3c
commit 02f5d4efbe
13 changed files with 264 additions and 41 deletions

View File

@@ -31,7 +31,7 @@ public final class BlockStateCachedData {
public static final BlockStateCachedData SCAFFOLDING = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().collisionHeight(1).canPlaceAgainstMe());
public final boolean fullyWalkableTop;
public final Integer collisionHeightBlips;
private final int collisionHeightBlips;
public final boolean isAir;
public final boolean collidesWithPlayer;
@@ -52,7 +52,11 @@ public final class BlockStateCachedData {
this.isAir = builder.isAir();
this.fullyWalkableTop = builder.isFullyWalkableTop();
this.collidesWithPlayer = builder.isCollidesWithPlayer();
this.collisionHeightBlips = builder.collisionHeightBlips();
if (collidesWithPlayer) {
this.collisionHeightBlips = builder.collisionHeightBlips();
} else {
this.collisionHeightBlips = -1;
}
this.mustSneakWhenPlacingAgainstMe = builder.isMustSneakWhenPlacingAgainstMe();
this.options = Collections.unmodifiableList(builder.howCanIBePlaced());
@@ -60,6 +64,13 @@ public final class BlockStateCachedData {
this.againstMe = builder.placeAgainstMe();
}
public int collisionHeightBlips() {
if (Main.DEBUG && !collidesWithPlayer) { // confirmed and tested: when DEBUG is false, proguard removes this if in the first pass, then inlines the calls in the second pass, making this just as good as a field access in release builds
throw new IllegalStateException();
}
return collisionHeightBlips;
}
public boolean possibleAgainstMe(BlockStatePlacementOption placement) {
if (Main.fakePlacementForPerformanceTesting) {
return Main.RAND.nextInt(10) < 8;

View File

@@ -30,7 +30,7 @@ public class BlockStateCachedDataBuilder {
private boolean fullyWalkableTop;
private boolean collidesWithPlayer;
private boolean mustSneakWhenPlacingAgainstMe;
private boolean falling;
private boolean mustBePlacedBottomToTop;
/**
* Examples:
* <p>
@@ -76,6 +76,15 @@ public class BlockStateCachedDataBuilder {
return fullyWalkableTop;
}
/**
* The highest collision extension of this block possible
* <p>
* For example, should be 1 for stairs, even though part of the top face is really 0.5
* <p>
* Should be 1 for top slabs
* <p>
* Should be 1 for trapdoors because when they're open, they touch the top face of the voxel
*/
public BlockStateCachedDataBuilder collisionHeight(double y) {
for (int h = 1; h <= Blip.PER_BLOCK + Blip.HALF_BLOCK; h++) {
if (y == h * Blip.RATIO) {
@@ -131,8 +140,8 @@ public class BlockStateCachedDataBuilder {
return this;
}
public BlockStateCachedDataBuilder falling() {
falling = true;
public BlockStateCachedDataBuilder mustBePlacedBottomToTop() {
mustBePlacedBottomToTop = true;
return this;
}
@@ -166,7 +175,7 @@ public class BlockStateCachedDataBuilder {
if (playerMustBeEntityFacingInOrderToPlaceMe == face) {
continue;
}
if (falling && face != Face.DOWN) {
if (mustBePlacedBottomToTop && face != Face.DOWN) {
continue;
}
if (canOnlyPlaceAgainst != null && face != canOnlyPlaceAgainst) {
@@ -214,6 +223,14 @@ public class BlockStateCachedDataBuilder {
return new PlaceAgainstData(face, face.vertical ? Half.EITHER : mustBePlacedAgainst, mustSneakWhenPlacingAgainstMe);
}
/**
* The idea here is that I codify all my assumptions in one place instead of having ad hoc checks absolutely everywhere
* <p>
* Example: in PlayerPhysics, I made an assumption that a block will never have a collision block taller than 1.5 blocks (e.g. like a fence)
* When I wrote the code that assumed that, I also added a check here to make sure every block is like that.
* If, in some future update to Minecraft, mojang adds a block that's even taller than a fence, it will be caught here immediately, with a comment saying "playerphysics assumes this is never true"
* This way, I'll know immediately, instead of pathing randomly trying to do something impossible with that new block and it being really confusing and annoying.
*/
public void sanityCheck() {
if (isAir()) {
if (!howCanIBePlaced().isEmpty()) {
@@ -246,17 +263,10 @@ public class BlockStateCachedDataBuilder {
if ((playerMustBeHorizontalFacingInOrderToPlaceMe != null || playerMustBeEntityFacingInOrderToPlaceMe != null) && mustBePlacedAgainst == null) {
throw new IllegalStateException();
}
if (isFullyWalkableTop() ^ collisionHeightBlips != null) {
if (!isFullyWalkableTop() && collisionHeightBlips > Blip.PER_BLOCK) {
// exception for fences, walls
} else {
throw new IllegalStateException();
}
}
if (collisionHeightBlips != null && (collisionHeightBlips > Blip.FULL_BLOCK + Blip.HALF_BLOCK || collisionHeightBlips <= 0)) { // playerphysics assumes this is never true
throw new IllegalStateException();
}
if (collidesWithPlayer && collisionHeightBlips == null) {
if (collidesWithPlayer ^ collisionHeightBlips != null) {
throw new IllegalStateException();
}
if (fullyWalkableTop && !collidesWithPlayer) {

View File

@@ -147,7 +147,7 @@ public class BlockStatePlacementOption {
if (playerMustBeHorizontalFacing.isPresent()) {
return eye.flatDirectionTo(hit) == playerMustBeHorizontalFacing.get();
}
if (playerMustBeEntityFacing.isPresent()) { // handle piston, dispenser, observer
if (playerMustBeEntityFacing.isPresent()) { // handle piston, dispenser, dropper, observer
if (!hit.inOriginUnitVoxel()) {
throw new IllegalStateException();
}
@@ -156,7 +156,7 @@ public class BlockStatePlacementOption {
double dx = Math.abs(eye.x - 0.5);
double dz = Math.abs(eye.z - 0.5);
if (dx < 2 - ENTITY_FACING_TOLERANCE && dz < 2 - ENTITY_FACING_TOLERANCE) { // < 1.99
if (eye.y < 0) { // eye below placement level = it will be facing down, so this is only okay if we wantthat
if (eye.y < 0) { // eye below placement level = it will be facing down, so this is only okay if we want that
return entFace == Face.DOWN;
}
if (eye.y > 2) { // same for up, if y>2 then it will be facing up
@@ -165,7 +165,7 @@ public class BlockStatePlacementOption {
} else if (!(dx > 2 + ENTITY_FACING_TOLERANCE || dz > 2 + ENTITY_FACING_TOLERANCE)) { // > 2.01
// this is the ambiguous case, because we are neither unambiguously both-within-2 (previous case), nor unambiguously either-above-two (this elseif condition).
// UP/DOWN are impossible, but that's caught by flat check
if (eye.y < 0 || eye.y > 2) {
if (eye.y < 0 || eye.y > 2) { // this check is okay because player eye height is not an even multiple of blips, therefore there's no way for it to == 0 or == 2, so using > and < is safe
return false; // anything that could cause up/down instead of horizontal is also not allowed sadly
}
} // else we are in unambiguous either-above-two, putting us in simple horizontal mode, so fallthrough to flat condition is correct, yay

View File

@@ -61,10 +61,11 @@ public class CuboidBounds {
}
public boolean inRange(int x, int y, int z) {
throw new UnsupportedOperationException("ugh benchmark this tomorrow when im less tired");
return inRangeBranchless(x, y, z);
}
public boolean inRangeBranchy(int x, int y, int z) {
@Deprecated
public boolean inRangeBranchy(int x, int y, int z) { // benchmarked: approx 4x slower than branchless
return (x >= 0) && (x < sizeX) && (y >= 0) && (y < sizeY) && (z >= 0) && (z < sizeZ);
}
@@ -72,6 +73,18 @@ public class CuboidBounds {
return (x | y | z | (sizeXMinusOne - x) | (sizeYMinusOne - y) | (sizeZMinusOne - z)) >= 0;
}
public boolean inRangeBranchless2(int x, int y, int z) {
return (x | y | z | ((sizeX - 1) - x) | ((sizeY - 1) - y) | ((sizeZ - 1) - z)) >= 0;
}
public boolean inRangeBranchless3(int x, int y, int z) {
return (x | y | z | (sizeX - (x + 1)) | (sizeY - (y + 1)) | (sizeZ - (z + 1))) >= 0;
}
public boolean inRangeBranchless4(int x, int y, int z) {
return (x | y | z | ((sizeX - x) - 1) | ((sizeY - y) - 1) | ((sizeZ - z) - 1)) >= 0;
}
public boolean inRangeIndex(int index) {
return (index | (sizeMinusOne - index)) >= 0;
}

View File

@@ -56,6 +56,7 @@ public class DependencyGraphAnalyzer {
if (!locs.isEmpty()) {
throw new IllegalStateException("Unplaceable from any side: " + cuteTrim(locs));
}
// TODO instead of cuteTrim have a like SpecificBlockPositionsImpossibleException that this throws, and then later, an enclosing function can give the option to reset those locations to air
}
/**

View File

@@ -17,8 +17,6 @@
package baritone.builder;
import java.util.Arrays;
public interface IBlockStateDataProvider {
int numStates();
@@ -27,7 +25,20 @@ public interface IBlockStateDataProvider {
default BlockStateCachedData[] allNullable() {
BlockStateCachedData[] ret = new BlockStateCachedData[numStates()];
Arrays.setAll(ret, this::getNullable);
RuntimeException ex = null;
for (int i = 0; i < ret.length; i++) {
try {
ret[i] = getNullable(i);
} catch (RuntimeException e) {
if (ex != null) {
ex.printStackTrace(); // printstacktrace all but the one that we throw
}
ex = e;
}
}
if (ex != null) {
throw ex; // throw the last one
}
return ret;
}
}

View File

@@ -266,6 +266,133 @@ public class Main {
{
DebugStates.debug();
}
{
Random rand = new Random(5021);
int trials = 10_000_000;
int[] X = new int[trials];
int[] Y = new int[trials];
int[] Z = new int[trials];
int sz = 10;
CuboidBounds bounds = new CuboidBounds(sz, sz, sz);
for (int i = 0; i < trials; i++) {
for (int[] toAdd : new int[][]{X, Y, Z}) {
toAdd[i] = rand.nextBoolean() ? rand.nextInt(sz) : rand.nextBoolean() ? -1 : sz;
}
}
boolean[] a = new boolean[trials];
boolean[] b = new boolean[trials];
boolean[] c = new boolean[trials];
boolean[] d = new boolean[trials];
boolean[] e = new boolean[trials];
for (int it = 0; it < 20; it++) {
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
a[i] = bounds.inRangeBranchy(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchy took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
b[i] = bounds.inRangeBranchless(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
c[i] = bounds.inRangeBranchless2(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless2 took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
d[i] = bounds.inRangeBranchless3(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless3 took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
e[i] = bounds.inRangeBranchless4(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless4 took " + (end - start) + "ms");
}
/*
Branchless2 took 55ms
Branchless3 took 53ms
Branchless4 took 47ms
Branchy took 137ms
Branchless took 35ms
Branchless2 took 36ms
Branchless3 took 35ms
Branchless4 took 41ms
Branchy took 118ms
Branchless took 33ms
Branchless2 took 39ms
Branchless3 took 36ms
Branchless4 took 42ms
Branchy took 125ms
Branchless took 41ms
Branchless2 took 45ms
Branchless3 took 41ms
Branchless4 took 45ms
Branchy took 123ms
Branchless took 38ms
Branchless2 took 43ms
Branchless3 took 35ms
Branchless4 took 43ms
Branchy took 117ms
Branchless took 37ms
Branchless2 took 42ms
Branchless3 took 41ms
Branchless4 took 45ms
Branchy took 123ms
Branchless took 35ms
Branchless2 took 42ms
Branchless3 took 38ms
Branchless4 took 46ms
Branchy took 126ms
Branchless took 34ms
Branchless2 took 47ms
Branchless3 took 40ms
Branchless4 took 47ms
Branchy took 124ms
*/
// 3 is better than 2 and 4 because of data dependency
// the L1 cache fetch for this.sizeX can happen at the same time as "x+1" (which is an increment of an argument)
// in other words: in options 2 and 4, the "+1" or "-1" has a data dependency on the RAM fetch for this.sizeX, but in option 3 alone, the +1 happens upon the argument x, which is likely in a register, meaning it can be pipelined in parallel with the L1 cache fetch for this.sizeX
}
}
/*{ // proguard test
PlayerPhysics.determinePlayerRealSupport(BlockStateCachedData.get(69), BlockStateCachedData.get(420));
PlayerPhysics.determinePlayerRealSupport(BlockStateCachedData.get(420), BlockStateCachedData.get(69));
}*/
System.exit(0);
}
}

View File

@@ -52,6 +52,12 @@ public class PlaceAgainstData {
this.top = top;
this.bottom = bottom;
this.hits = hits;
if (!streamRelativeToMyself().allMatch(Vec3d::inOriginUnitVoxel)) {
throw new IllegalStateException();
}
if (!streamRelativeToPlace().allMatch(Vec3d::inOriginUnitVoxel)) {
throw new IllegalStateException();
}
}
public PlaceAgainstData(Face against, Half half, boolean mustSneak) {

View File

@@ -26,21 +26,21 @@ public class PlayerPhysics {
*/
public static int determinePlayerRealSupport(BlockStateCachedData underneath, BlockStateCachedData within) {
if (within.collidesWithPlayer) {
if (underneath.collisionHeightBlips != null && underneath.collisionHeightBlips - Blip.FULL_BLOCK > within.collisionHeightBlips) { // TODO > or >=
if (underneath.collidesWithPlayer && underneath.collisionHeightBlips() - Blip.FULL_BLOCK > within.collisionHeightBlips()) { // > because imagine something like slab on top of fence, we can walk on the slab even though the fence is equivalent height
if (!underneath.fullyWalkableTop) {
return -1;
}
return underneath.collisionHeightBlips - Blip.FULL_BLOCK; // this could happen if "underneath" is a fence and "within" is a carpet
return underneath.collisionHeightBlips() - Blip.FULL_BLOCK; // this could happen if "underneath" is a fence and "within" is a carpet
}
if (!within.fullyWalkableTop || within.collisionHeightBlips >= Blip.FULL_BLOCK) {
if (!within.fullyWalkableTop || within.collisionHeightBlips() >= Blip.FULL_BLOCK) {
return -1;
}
return within.collisionHeightBlips;
return within.collisionHeightBlips();
} else {
if (!underneath.fullyWalkableTop || underneath.collisionHeightBlips < Blip.FULL_BLOCK) {
if (!underneath.fullyWalkableTop || underneath.collisionHeightBlips() < Blip.FULL_BLOCK) { // short circuit only calls collisionHeightBlips when fullyWalkableTop is true, so this is safe
return -1;
}
return underneath.collisionHeightBlips - Blip.FULL_BLOCK;
return underneath.collisionHeightBlips() - Blip.FULL_BLOCK;
}
}
@@ -119,13 +119,10 @@ public class PlayerPhysics {
if (!D.collidesWithPlayer) {
return Collision.FALL;
}
if (Main.DEBUG && D.collisionHeightBlips == null) {
if (Main.DEBUG && D.collisionHeightBlips() >= Blip.FULL_BLOCK && D.fullyWalkableTop) {
throw new IllegalStateException();
}
if (Main.DEBUG && D.collisionHeightBlips >= Blip.FULL_BLOCK && D.fullyWalkableTop) {
throw new IllegalStateException();
}
if (D.collisionHeightBlips < Blip.FULL_BLOCK + feet) {
if (D.collisionHeightBlips() < Blip.FULL_BLOCK + feet) {
return Collision.FALL;
} else {
return Collision.BLOCKED;
@@ -143,5 +140,6 @@ public class PlayerPhysics {
VOXEL_UP, // if you hit W, you will end up at a position that's a bit higher, such that you'd determineRealPlayerSupport up by one (example: walking from a partial block to a full block or higher, e.g. half slab to full block, or soul sand to full block, or soul sand to full block+carpet on top)
VOXEL_LEVEL, // if you hit W, you will end up at a similar position, such that you'd determineRealPlayerSupport at the same integer grid location (example: walking forward on level ground)
FALL // if you hit W, you will not immediately collide with anything, at all, to the front or to the bottom (example: walking off a cliff)
// TODO maybe we need another option that is like "you could do it, but you shouldn't". like, "if you hit W, you would walk forward, but you wouldn't like the outcome" such as cactus or lava or something
}
}

View File

@@ -52,9 +52,13 @@ public class Scaffolder {
this.components = collapsedGraph.getComponents();
this.componentLocations = collapsedGraph.getComponentLocations();
this.rootComponents = calcRoots();
}
private List<CollapsedDependencyGraphComponent> calcRoots() {
// since the components form a DAG (because all strongly connected components, and therefore all cycles, have been collapsed)
// we can locate all root components by simply finding the ones with no incoming edges
this.rootComponents = components
return components
.values()
.stream()
.filter(component -> component.getIncoming().isEmpty())
@@ -62,6 +66,7 @@ public class Scaffolder {
}
private void loop() {
int cid = collapsedGraph.lastComponentID().getAsInt();
CollapsedDependencyGraphComponent root = rootComponents.remove(rootComponents.size() - 1);
if (!root.getIncoming().isEmpty()) {
throw new IllegalStateException();
@@ -84,6 +89,24 @@ public class Scaffolder {
}
}
for (int i = 1; i < path.size() - 1; i++) {
overlayGraph.enable(path.get(i).pos);
}
int newCID = collapsedGraph.lastComponentID().getAsInt();
for (int i = cid + 1; i <= newCID; i++) {
if (components.get(i) != null && components.get(i).getIncoming().isEmpty()) {
rootComponents.add(components.get(i));
}
}
// this works because as we add new components and connect them up, we can say that
rootComponents.removeIf(CollapsedDependencyGraphComponent::deleted);
if (Main.DEBUG) {
if (!rootComponents.equals(calcRoots())) {
throw new IllegalStateException();
}
}
}
private void walkAllDescendents(CollapsedDependencyGraphComponent root, Set<CollapsedDependencyGraphComponent> set) {

View File

@@ -61,8 +61,10 @@ public class Vec3d {
return new Vec3d(dst.x - x, dst.y - y, dst.z - z).flatDirection();
}
private static final double AMBIGUITY_TOLERANCE = 0.01;
public Face flatDirection() {
if (Math.abs(x) == Math.abs(z)) {
if (Math.abs(Math.abs(x) - Math.abs(z)) < AMBIGUITY_TOLERANCE) {
throw new IllegalStateException("ambiguous");
}
if (Math.abs(x) > Math.abs(z)) {

View File

@@ -60,10 +60,10 @@ public class BlockStatePropertiesExtractor {
};
if (!rightsideUp) {
stairBuilder.fullyWalkableTop();
stairBuilder.collisionHeight(1);
}
return stairBuilder.mustBePlacedAgainst(rightsideUp ? Half.BOTTOM : Half.TOP)
.collidesWithPlayer(true)
.collisionHeight(1)
.canPlaceAgainstMe()
.playerMustBeHorizontalFacingInOrderToPlaceMe(facing);
}
@@ -93,7 +93,9 @@ public class BlockStatePropertiesExtractor {
ret.add(BlockStatePlacementOption.get(facing.opposite(), bottom ? Half.BOTTOM : Half.TOP, Optional.empty(), Optional.empty()));
return ret;
}
}.collidesWithPlayer(true); // dont allow walking on top of closed top-half trapdoor because redstone activation is scary and im not gonna predict it
}
.collisionHeight(1) // sometimes it can be 1, and for collision height we err on the side of max
.collidesWithPlayer(true); // dont allow walking on top of closed top-half trapdoor because redstone activation is scary and im not gonna predict it
}
if (block instanceof BlockLog) {
BlockLog.EnumAxis axis = state.getValue(BlockLog.LOG_AXIS);
@@ -168,6 +170,7 @@ public class BlockStatePropertiesExtractor {
{
if (block instanceof BlockContainer || block instanceof BlockWorkbench) {
// TODO way more blocks have a right click action, e.g. redstone repeater, daylight sensor
builder.mustSneakWhenPlacingAgainstMe();
}
}
@@ -210,7 +213,7 @@ public class BlockStatePropertiesExtractor {
}
if (block instanceof BlockFalling) {
builder.falling();
builder.mustBePlacedBottomToTop();
}
@@ -221,6 +224,11 @@ public class BlockStatePropertiesExtractor {
// getStateForPlacement.against is the against face. placing a torch will have it as UP. placing a bottom slab will have it as UP. placing a top slab will have it as DOWN.
if (block instanceof BlockFence || (block instanceof BlockFenceGate && !state.getValue(BlockFenceGate.OPEN)) || block instanceof BlockWall) {
builder.collisionHeight(1.5);
fullyUnderstood = true;
}
if (block instanceof BlockTorch) { // includes redstone torch
builder.canOnlyPlaceAgainst(Face.fromMC(state.getValue(BlockTorch.FACING)).opposite());
fullyUnderstood = true;
@@ -228,6 +236,7 @@ public class BlockStatePropertiesExtractor {
if (block instanceof BlockShulkerBox) {
builder.canOnlyPlaceAgainst(Face.fromMC(state.getValue(BlockShulkerBox.FACING)).opposite());
builder.collisionHeight(1); // TODO should this be 1.5 because sometimes the shulker is open?
fullyUnderstood = true;
}
@@ -244,7 +253,9 @@ public class BlockStatePropertiesExtractor {
|| block instanceof BlockRailBase
|| block instanceof BlockFlower
|| block instanceof BlockDeadBush
|| block instanceof BlockMushroom
) {
builder.mustBePlacedBottomToTop();
fullyUnderstood = true;
}
@@ -254,8 +265,11 @@ public class BlockStatePropertiesExtractor {
fullyUnderstood = true;
}
if ((state.isBlockNormalCube() || block instanceof BlockGlass || block instanceof BlockStainedGlass) && !(block instanceof BlockMagma || block instanceof BlockSlime)) {
builder.fullyWalkableTop().collisionHeight(1);
if (state.isBlockNormalCube() || block instanceof BlockGlass || block instanceof BlockStainedGlass) {
builder.collisionHeight(1);
if (!(block instanceof BlockMagma || block instanceof BlockSlime)) {
builder.fullyWalkableTop();
}
fullyUnderstood = true;
}
@@ -272,6 +286,11 @@ public class BlockStatePropertiesExtractor {
fullyUnderstood = true;
}
if (block instanceof BlockGrassPath || block instanceof BlockFarmland) {
builder.collisionHeight(0.9375);
fullyUnderstood = true;
}
// TODO fully walkable top and height
if (fullyUnderstood) {

View File

@@ -103,6 +103,8 @@ public class DebugStates {
props.put("placeme", "" + data.options.size());
props.put("sneak", "" + data.mustSneakWhenPlacingAgainstMe);
props.put("againstme", "" + Stream.of(data.againstMe).filter(Objects::nonNull).count());
props.put("y", "" + data.collisionHeightBlips);
if (data.collidesWithPlayer) {
props.put("y", "" + data.collisionHeightBlips());
}
}
}