CC-Tweaked/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

620 lines
22 KiB
Java
Raw Normal View History

// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.peripheral.monitor;
import com.google.common.annotations.VisibleForTesting;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import dan200.computercraft.shared.util.TickScheduler;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
public class MonitorBlockEntity extends BlockEntity {
private static final Logger LOG = LoggerFactory.getLogger(MonitorBlockEntity.class);
public static final double RENDER_BORDER = 2.0 / 16.0;
public static final double RENDER_MARGIN = 0.5 / 16.0;
public static final double RENDER_PIXEL_SCALE = 1.0 / 64.0;
private static final String NBT_X = "XIndex";
private static final String NBT_Y = "YIndex";
private static final String NBT_WIDTH = "Width";
private static final String NBT_HEIGHT = "Height";
private final boolean advanced;
private @Nullable ServerMonitor serverMonitor;
/**
* The monitor's state on the client. This is defined iff we're the origin monitor
* ({@code xIndex == 0 && yIndex == 0}).
*/
private @Nullable ClientMonitor clientMonitor;
private @Nullable MonitorPeripheral peripheral;
private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
private boolean needsUpdate = false;
private boolean needsValidating = false;
// MonitorWatcher state.
boolean enqueued;
@Nullable
TerminalState cached;
private int width = 1;
private int height = 1;
private int xIndex = 0;
private int yIndex = 0;
private @Nullable BlockPos bbPos;
private @Nullable BlockState bbState;
Cleanup and optimise terminal rendering (#1057) - Remove the POSITION_COLOR render type. Instead we just render a background terminal quad as the pocket computer light - it's a little (lot?) more cheaty, but saves having to create a render type. - Use the existing position_color_tex shader instead of our copy. I looked at using RenderType.text, but had a bunch of problems with GUI terminals. Its possible we can fix it, but didn't want to spend too much time on it. - Remove some methods from FixedWidthFontRenderer, inlining them into the call site. - Switch back to using GL_QUADS rather than GL_TRIANGLES. I know Lig will shout at me for this, but the rest of MC uses QUADS, so I don't think best practice really matters here. - Fix the TBO backend monitor not rendering monitors with fog. Unfortunately we can't easily do this to the VBO one without writing a custom shader (which defeats the whole point of the VBO backend!), as the distance calculation of most render types expect an already-transformed position (camera-relative I think!) while we pass a world-relative one. - When rendering to a VBO we push vertices to a ByteBuffer directly, rather than going through MC's VertexConsumer system. This removes the overhead which comes with VertexConsumer, significantly improving performance. - Pre-convert palette colours to bytes, storing both the coloured and greyscale versions as a byte array. This allows us to remove the multiple casts and conversions (double -> float -> (greyscale) -> byte), offering noticeable performance improvements (multiple ms per frame). We're using a byte[] here rather than a record of three bytes as notionally it provides better performance when writing to a ByteBuffer directly compared to calling .put() four times. [^1] - Memorize getRenderBoundingBox. This was taking about 5% of the total time on the render thread[^2], so worth doing. I don't actually think the allocation is the heavy thing here - VisualVM says it's toWorldPos being slow. I'm not sure why - possibly just all the block property lookups? [^2] Note that none of these changes improve compatibility with Optifine. Right now there's some serious issues where monitors are writing _over_ blocks in front of them. To fix this, we probably need to remove the depth blocker and just render characters with a z offset. Will do that in a separate commit, as I need to evaluate how well that change will work first. The main advantage of this commit is the improved performance. In my stress test with 120 monitors updating every tick, I'm getting 10-20fps [^3] (still much worse than TBOs, which manages a solid 60-100). In practice, we'll actually be much better than this. Our network bandwidth limits means only 40 change in a single tick - and so FPS is much more reasonable (+60fps). [^1]: In general, put(byte[]) is faster than put(byte) multiple times. Just not clear if this is true when dealing with a small (and loop unrolled) number of bytes. [^2]: To be clear, this is with 120 monitors and no other block entities with custom renderers. so not really representative. [^3]: I wish I could provide a narrower range, but it varies so much between me restarting the game. Makes it impossible to benchmark anything!
2022-04-02 09:54:03 +00:00
private int bbX, bbY, bbWidth, bbHeight;
private @Nullable AABB boundingBox;
Cleanup and optimise terminal rendering (#1057) - Remove the POSITION_COLOR render type. Instead we just render a background terminal quad as the pocket computer light - it's a little (lot?) more cheaty, but saves having to create a render type. - Use the existing position_color_tex shader instead of our copy. I looked at using RenderType.text, but had a bunch of problems with GUI terminals. Its possible we can fix it, but didn't want to spend too much time on it. - Remove some methods from FixedWidthFontRenderer, inlining them into the call site. - Switch back to using GL_QUADS rather than GL_TRIANGLES. I know Lig will shout at me for this, but the rest of MC uses QUADS, so I don't think best practice really matters here. - Fix the TBO backend monitor not rendering monitors with fog. Unfortunately we can't easily do this to the VBO one without writing a custom shader (which defeats the whole point of the VBO backend!), as the distance calculation of most render types expect an already-transformed position (camera-relative I think!) while we pass a world-relative one. - When rendering to a VBO we push vertices to a ByteBuffer directly, rather than going through MC's VertexConsumer system. This removes the overhead which comes with VertexConsumer, significantly improving performance. - Pre-convert palette colours to bytes, storing both the coloured and greyscale versions as a byte array. This allows us to remove the multiple casts and conversions (double -> float -> (greyscale) -> byte), offering noticeable performance improvements (multiple ms per frame). We're using a byte[] here rather than a record of three bytes as notionally it provides better performance when writing to a ByteBuffer directly compared to calling .put() four times. [^1] - Memorize getRenderBoundingBox. This was taking about 5% of the total time on the render thread[^2], so worth doing. I don't actually think the allocation is the heavy thing here - VisualVM says it's toWorldPos being slow. I'm not sure why - possibly just all the block property lookups? [^2] Note that none of these changes improve compatibility with Optifine. Right now there's some serious issues where monitors are writing _over_ blocks in front of them. To fix this, we probably need to remove the depth blocker and just render characters with a z offset. Will do that in a separate commit, as I need to evaluate how well that change will work first. The main advantage of this commit is the improved performance. In my stress test with 120 monitors updating every tick, I'm getting 10-20fps [^3] (still much worse than TBOs, which manages a solid 60-100). In practice, we'll actually be much better than this. Our network bandwidth limits means only 40 change in a single tick - and so FPS is much more reasonable (+60fps). [^1]: In general, put(byte[]) is faster than put(byte) multiple times. Just not clear if this is true when dealing with a small (and loop unrolled) number of bytes. [^2]: To be clear, this is with 120 monitors and no other block entities with custom renderers. so not really representative. [^3]: I wish I could provide a narrower range, but it varies so much between me restarting the game. Makes it impossible to benchmark anything!
2022-04-02 09:54:03 +00:00
TickScheduler.Token tickToken = new TickScheduler.Token(this);
public MonitorBlockEntity(BlockEntityType<? extends MonitorBlockEntity> type, BlockPos pos, BlockState state, boolean advanced) {
super(type, pos, state);
this.advanced = advanced;
}
@Override
public void clearRemoved() {
super.clearRemoved();
needsValidating = true; // Same, tbh
TickScheduler.schedule(tickToken);
}
void destroy() {
// TODO: Call this before using the block
if (!getLevel().isClientSide) contractNeighbours();
}
@Override
public void setRemoved() {
super.setRemoved();
if (clientMonitor != null) clientMonitor.destroy();
}
@Override
public void saveAdditional(CompoundTag tag) {
tag.putInt(NBT_X, xIndex);
tag.putInt(NBT_Y, yIndex);
tag.putInt(NBT_WIDTH, width);
tag.putInt(NBT_HEIGHT, height);
super.saveAdditional(tag);
}
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
var oldXIndex = xIndex;
var oldYIndex = yIndex;
xIndex = nbt.getInt(NBT_X);
yIndex = nbt.getInt(NBT_Y);
width = nbt.getInt(NBT_WIDTH);
height = nbt.getInt(NBT_HEIGHT);
if (level != null && level.isClientSide) onClientLoad(oldXIndex, oldYIndex);
}
void blockTick() {
if (needsValidating) {
needsValidating = false;
validate();
}
if (needsUpdate) {
needsUpdate = false;
expand();
}
if (xIndex != 0 || yIndex != 0 || serverMonitor == null) return;
if (serverMonitor.pollResized()) eachComputer(c -> c.queueEvent("monitor_resize", c.getAttachmentName()));
if (serverMonitor.pollTerminalChanged()) MonitorWatcher.enqueue(this);
2017-05-01 14:48:44 +00:00
}
@Nullable
@VisibleForTesting
public ServerMonitor getCachedServerMonitor() {
return serverMonitor;
}
@Nullable
private ServerMonitor getServerMonitor() {
if (serverMonitor != null) return serverMonitor;
var origin = getOrigin();
if (origin == null) return null;
return serverMonitor = origin.serverMonitor;
}
@Nullable
private ServerMonitor createServerMonitor() {
if (serverMonitor != null) return serverMonitor;
if (xIndex == 0 && yIndex == 0) {
// If we're the origin, set up the new monitor
serverMonitor = new ServerMonitor(advanced, this);
// And propagate it to child monitors
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var monitor = getLoadedMonitor(x, y).getMonitor();
if (monitor != null) monitor.serverMonitor = serverMonitor;
}
}
return serverMonitor;
} else {
// Otherwise fetch the origin and attempt to get its monitor
// Note this may load chunks, but we don't really have a choice here.
var te = getLevel().getBlockEntity(toWorldPos(0, 0));
if (!(te instanceof MonitorBlockEntity monitor)) return null;
return serverMonitor = monitor.createServerMonitor();
}
}
private void createServerTerminal() {
var monitor = createServerMonitor();
if (monitor != null && monitor.getTerminal() == null) monitor.rebuild();
}
@Nullable
public ClientMonitor getOriginClientMonitor() {
if (clientMonitor != null) return clientMonitor;
var origin = getOrigin();
return origin == null ? null : origin.clientMonitor;
}
2017-05-01 14:48:44 +00:00
// Networking stuff
@Override
public final ClientboundBlockEntityDataPacket getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public final CompoundTag getUpdateTag() {
var nbt = super.getUpdateTag();
nbt.putInt(NBT_X, xIndex);
nbt.putInt(NBT_Y, yIndex);
nbt.putInt(NBT_WIDTH, width);
nbt.putInt(NBT_HEIGHT, height);
return nbt;
}
private void onClientLoad(int oldXIndex, int oldYIndex) {
if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
// If our index has changed, and we were the origin, then destroy the current monitor.
clientMonitor.destroy();
clientMonitor = null;
}
// If we're the origin terminal then create it.
if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
}
public final void read(@Nullable TerminalState state) {
if (xIndex != 0 || yIndex != 0) {
LOG.warn("Receiving monitor state for non-origin terminal at {}", getBlockPos());
return;
}
if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
clientMonitor.read(state);
}
// Sizing and placement stuff
private void updateBlockState() {
getLevel().setBlock(getBlockPos(), getBlockState()
.setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
yIndex < height - 1, yIndex > 0,
xIndex > 0, xIndex < width - 1)), 2);
}
// region Sizing and placement stuff
public Direction getDirection() {
// Ensure we're actually a monitor block. This _should_ always be the case, but sometimes there's
// fun problems with the block being missing on the client.
var state = getBlockState();
return state.hasProperty(MonitorBlock.FACING) ? state.getValue(MonitorBlock.FACING) : Direction.NORTH;
}
public Direction getOrientation() {
var state = getBlockState();
return state.hasProperty(MonitorBlock.ORIENTATION) ? state.getValue(MonitorBlock.ORIENTATION) : Direction.NORTH;
}
public Direction getFront() {
var orientation = getOrientation();
return orientation == Direction.NORTH ? getDirection() : orientation;
}
public Direction getRight() {
return getDirection().getCounterClockWise();
}
public Direction getDown() {
var orientation = getOrientation();
if (orientation == Direction.NORTH) return Direction.UP;
return orientation == Direction.DOWN ? getDirection() : getDirection().getOpposite();
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getXIndex() {
return xIndex;
}
public int getYIndex() {
return yIndex;
}
boolean isCompatible(MonitorBlockEntity other) {
return advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
}
/**
* Get a tile within the current monitor only if it is loaded and compatible.
*
* @param x Absolute X position in monitor coordinates
* @param y Absolute Y position in monitor coordinates
* @return The located monitor
*/
private MonitorState getLoadedMonitor(int x, int y) {
if (x == xIndex && y == yIndex) return MonitorState.present(this);
var pos = toWorldPos(x, y);
var world = getLevel();
if (world == null || !world.isLoaded(pos)) return MonitorState.UNLOADED;
var tile = world.getBlockEntity(pos);
if (!(tile instanceof MonitorBlockEntity monitor)) return MonitorState.MISSING;
return isCompatible(monitor) ? MonitorState.present(monitor) : MonitorState.MISSING;
}
private @Nullable MonitorBlockEntity getOrigin() {
return getLoadedMonitor(0, 0).getMonitor();
}
/**
* Convert monitor coordinates to world coordinates.
*
* @param x Absolute X position in monitor coordinates
* @param y Absolute Y position in monitor coordinates
* @return The monitor's position.
*/
BlockPos toWorldPos(int x, int y) {
if (xIndex == x && yIndex == y) return getBlockPos();
return getBlockPos().relative(getRight(), -xIndex + x).relative(getDown(), -yIndex + y);
}
void resize(int width, int height) {
2018-02-14 21:21:00 +00:00
// If we're not already the origin then we'll need to generate a new terminal.
if (xIndex != 0 || yIndex != 0) serverMonitor = null;
2018-02-14 21:21:00 +00:00
xIndex = 0;
yIndex = 0;
this.width = width;
this.height = height;
// Determine if we actually need a monitor. In order to do this, simply check if
// any component monitor been wrapped as a peripheral. Whilst this flag may be
// out of date,
var needsTerminal = false;
terminalCheck:
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var monitor = getLoadedMonitor(x, y).getMonitor();
if (monitor != null && monitor.peripheral != null) {
needsTerminal = true;
break terminalCheck;
2017-05-01 14:48:44 +00:00
}
}
}
// Either delete the current monitor or sync a new one.
if (needsTerminal) {
if (serverMonitor == null) serverMonitor = new ServerMonitor(advanced, this);
// Update the terminal's width and height and rebuild it. This ensures the monitor
// is consistent when syncing it to other monitors.
serverMonitor.rebuild();
} else {
// Remove the terminal from the serverMonitor, but keep it around - this ensures that we sync
// the (now blank) monitor to the client.
if (serverMonitor != null) serverMonitor.reset();
}
// Update the other monitors, setting coordinates, dimensions and the server terminal
var pos = getBlockPos();
Direction down = getDown(), right = getRight();
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var other = getLevel().getBlockEntity(pos.relative(right, x).relative(down, y));
if (!(other instanceof MonitorBlockEntity monitor) || !isCompatible(monitor)) continue;
monitor.xIndex = x;
monitor.yIndex = y;
monitor.width = width;
monitor.height = height;
monitor.serverMonitor = serverMonitor;
monitor.needsUpdate = monitor.needsValidating = false;
monitor.updateBlockState();
BlockEntityHelpers.updateBlock(monitor);
}
}
assertInvariant();
2017-05-01 14:48:44 +00:00
}
void updateNeighborsDeferred() {
needsUpdate = true;
}
void expand() {
var monitor = getOrigin();
if (monitor != null && monitor.xIndex == 0 && monitor.yIndex == 0) new Expander(monitor).expand();
2017-05-01 14:48:44 +00:00
}
private void contractNeighbours() {
if (width == 1 && height == 1) return;
var pos = getBlockPos();
Direction down = getDown(), right = getRight();
var origin = toWorldPos(0, 0);
MonitorBlockEntity toLeft = null, toAbove = null, toRight = null, toBelow = null;
if (xIndex > 0) toLeft = tryResizeAt(pos.relative(right, -xIndex), xIndex, 1);
if (yIndex > 0) toAbove = tryResizeAt(origin, width, yIndex);
if (xIndex < width - 1) toRight = tryResizeAt(pos.relative(right, 1), width - xIndex - 1, 1);
if (yIndex < height - 1) {
toBelow = tryResizeAt(origin.relative(down, yIndex + 1), width, height - yIndex - 1);
2017-05-01 14:48:44 +00:00
}
if (toLeft != null) toLeft.expand();
if (toAbove != null) toAbove.expand();
if (toRight != null) toRight.expand();
if (toBelow != null) toBelow.expand();
2017-05-01 14:48:44 +00:00
}
@Nullable
private MonitorBlockEntity tryResizeAt(BlockPos pos, int width, int height) {
var tile = getLevel().getBlockEntity(pos);
if (tile instanceof MonitorBlockEntity monitor && isCompatible(monitor)) {
monitor.resize(width, height);
return monitor;
2017-05-01 14:48:44 +00:00
}
return null;
2017-05-01 14:48:44 +00:00
}
private boolean checkMonitorAt(int xIndex, int yIndex) {
var state = getLoadedMonitor(xIndex, yIndex);
if (state.isMissing()) return false;
var monitor = state.getMonitor();
if (monitor == null) return true;
return monitor.xIndex == xIndex && monitor.yIndex == yIndex && monitor.width == width && monitor.height == height;
}
private void validate() {
if (xIndex == 0 && yIndex == 0 && width == 1 && height == 1) return;
if (xIndex >= 0 && xIndex <= width && width > 0 && width <= Config.monitorWidth &&
yIndex >= 0 && yIndex <= height && height > 0 && height <= Config.monitorHeight &&
checkMonitorAt(0, 0) && checkMonitorAt(0, height - 1) &&
checkMonitorAt(width - 1, 0) && checkMonitorAt(width - 1, height - 1)) {
return;
}
// Something in our monitor is invalid. For now, let's just reset ourselves and then try to integrate ourselves
// later.
LOG.warn("Monitor is malformed, resetting to 1x1.");
resize(1, 1);
needsUpdate = true;
}
// endregion
void monitorTouched(float xPos, float yPos, float zPos) {
if (!advanced) return;
var pair = XYPair
.of(xPos, yPos, zPos, getDirection(), getOrientation())
.add(xIndex, height - yIndex - 1);
if (pair.x() > width - RENDER_BORDER || pair.y() > height - RENDER_BORDER || pair.x() < RENDER_BORDER || pair.y() < RENDER_BORDER) {
2017-05-01 14:48:44 +00:00
return;
}
var serverTerminal = getServerMonitor();
if (serverTerminal == null) return;
Terminal originTerminal = serverTerminal.getTerminal();
if (originTerminal == null) return;
var xCharWidth = (width - (RENDER_BORDER + RENDER_MARGIN) * 2.0) / originTerminal.getWidth();
var yCharHeight = (height - (RENDER_BORDER + RENDER_MARGIN) * 2.0) / originTerminal.getHeight();
var xCharPos = (int) Math.min(originTerminal.getWidth(), Math.max((pair.x() - RENDER_BORDER - RENDER_MARGIN) / xCharWidth + 1.0, 1.0));
var yCharPos = (int) Math.min(originTerminal.getHeight(), Math.max((pair.y() - RENDER_BORDER - RENDER_MARGIN) / yCharHeight + 1.0, 1.0));
eachComputer(c -> c.queueEvent("monitor_touch", c.getAttachmentName(), xCharPos, yCharPos));
}
private void eachComputer(Consumer<IComputerAccess> fun) {
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var monitor = getLoadedMonitor(x, y).getMonitor();
if (monitor == null) continue;
for (var computer : monitor.computers) fun.accept(computer);
2017-05-01 14:48:44 +00:00
}
}
}
public IPeripheral peripheral() {
createServerTerminal();
var peripheral = this.peripheral != null ? this.peripheral : (this.peripheral = new MonitorPeripheral(this));
assertInvariant();
return peripheral;
}
void addComputer(IComputerAccess computer) {
computers.add(computer);
2017-05-01 14:48:44 +00:00
}
void removeComputer(IComputerAccess computer) {
computers.remove(computer);
2017-05-01 14:48:44 +00:00
}
@ForgeOverride
public AABB getRenderBoundingBox() {
Cleanup and optimise terminal rendering (#1057) - Remove the POSITION_COLOR render type. Instead we just render a background terminal quad as the pocket computer light - it's a little (lot?) more cheaty, but saves having to create a render type. - Use the existing position_color_tex shader instead of our copy. I looked at using RenderType.text, but had a bunch of problems with GUI terminals. Its possible we can fix it, but didn't want to spend too much time on it. - Remove some methods from FixedWidthFontRenderer, inlining them into the call site. - Switch back to using GL_QUADS rather than GL_TRIANGLES. I know Lig will shout at me for this, but the rest of MC uses QUADS, so I don't think best practice really matters here. - Fix the TBO backend monitor not rendering monitors with fog. Unfortunately we can't easily do this to the VBO one without writing a custom shader (which defeats the whole point of the VBO backend!), as the distance calculation of most render types expect an already-transformed position (camera-relative I think!) while we pass a world-relative one. - When rendering to a VBO we push vertices to a ByteBuffer directly, rather than going through MC's VertexConsumer system. This removes the overhead which comes with VertexConsumer, significantly improving performance. - Pre-convert palette colours to bytes, storing both the coloured and greyscale versions as a byte array. This allows us to remove the multiple casts and conversions (double -> float -> (greyscale) -> byte), offering noticeable performance improvements (multiple ms per frame). We're using a byte[] here rather than a record of three bytes as notionally it provides better performance when writing to a ByteBuffer directly compared to calling .put() four times. [^1] - Memorize getRenderBoundingBox. This was taking about 5% of the total time on the render thread[^2], so worth doing. I don't actually think the allocation is the heavy thing here - VisualVM says it's toWorldPos being slow. I'm not sure why - possibly just all the block property lookups? [^2] Note that none of these changes improve compatibility with Optifine. Right now there's some serious issues where monitors are writing _over_ blocks in front of them. To fix this, we probably need to remove the depth blocker and just render characters with a z offset. Will do that in a separate commit, as I need to evaluate how well that change will work first. The main advantage of this commit is the improved performance. In my stress test with 120 monitors updating every tick, I'm getting 10-20fps [^3] (still much worse than TBOs, which manages a solid 60-100). In practice, we'll actually be much better than this. Our network bandwidth limits means only 40 change in a single tick - and so FPS is much more reasonable (+60fps). [^1]: In general, put(byte[]) is faster than put(byte) multiple times. Just not clear if this is true when dealing with a small (and loop unrolled) number of bytes. [^2]: To be clear, this is with 120 monitors and no other block entities with custom renderers. so not really representative. [^3]: I wish I could provide a narrower range, but it varies so much between me restarting the game. Makes it impossible to benchmark anything!
2022-04-02 09:54:03 +00:00
// We attempt to cache the bounding box to save having to do property lookups (and allocations!) on every frame.
// Unfortunately the AABB does depend on quite a lot of state, so we need to add a bunch of extra fields -
// ideally these'd be a single object, but I don't think worth doing until Java has value types.
if (boundingBox != null && getBlockState().equals(bbState) && getBlockPos().equals(bbPos) &&
xIndex == bbX && yIndex == bbY && width == bbWidth && height == bbHeight) {
return boundingBox;
}
bbState = getBlockState();
bbPos = getBlockPos();
bbX = xIndex;
bbY = yIndex;
bbWidth = width;
bbHeight = height;
var startPos = toWorldPos(0, 0);
var endPos = toWorldPos(width, height);
Cleanup and optimise terminal rendering (#1057) - Remove the POSITION_COLOR render type. Instead we just render a background terminal quad as the pocket computer light - it's a little (lot?) more cheaty, but saves having to create a render type. - Use the existing position_color_tex shader instead of our copy. I looked at using RenderType.text, but had a bunch of problems with GUI terminals. Its possible we can fix it, but didn't want to spend too much time on it. - Remove some methods from FixedWidthFontRenderer, inlining them into the call site. - Switch back to using GL_QUADS rather than GL_TRIANGLES. I know Lig will shout at me for this, but the rest of MC uses QUADS, so I don't think best practice really matters here. - Fix the TBO backend monitor not rendering monitors with fog. Unfortunately we can't easily do this to the VBO one without writing a custom shader (which defeats the whole point of the VBO backend!), as the distance calculation of most render types expect an already-transformed position (camera-relative I think!) while we pass a world-relative one. - When rendering to a VBO we push vertices to a ByteBuffer directly, rather than going through MC's VertexConsumer system. This removes the overhead which comes with VertexConsumer, significantly improving performance. - Pre-convert palette colours to bytes, storing both the coloured and greyscale versions as a byte array. This allows us to remove the multiple casts and conversions (double -> float -> (greyscale) -> byte), offering noticeable performance improvements (multiple ms per frame). We're using a byte[] here rather than a record of three bytes as notionally it provides better performance when writing to a ByteBuffer directly compared to calling .put() four times. [^1] - Memorize getRenderBoundingBox. This was taking about 5% of the total time on the render thread[^2], so worth doing. I don't actually think the allocation is the heavy thing here - VisualVM says it's toWorldPos being slow. I'm not sure why - possibly just all the block property lookups? [^2] Note that none of these changes improve compatibility with Optifine. Right now there's some serious issues where monitors are writing _over_ blocks in front of them. To fix this, we probably need to remove the depth blocker and just render characters with a z offset. Will do that in a separate commit, as I need to evaluate how well that change will work first. The main advantage of this commit is the improved performance. In my stress test with 120 monitors updating every tick, I'm getting 10-20fps [^3] (still much worse than TBOs, which manages a solid 60-100). In practice, we'll actually be much better than this. Our network bandwidth limits means only 40 change in a single tick - and so FPS is much more reasonable (+60fps). [^1]: In general, put(byte[]) is faster than put(byte) multiple times. Just not clear if this is true when dealing with a small (and loop unrolled) number of bytes. [^2]: To be clear, this is with 120 monitors and no other block entities with custom renderers. so not really representative. [^3]: I wish I could provide a narrower range, but it varies so much between me restarting the game. Makes it impossible to benchmark anything!
2022-04-02 09:54:03 +00:00
return boundingBox = new AABB(
Math.min(startPos.getX(), endPos.getX()),
Math.min(startPos.getY(), endPos.getY()),
Math.min(startPos.getZ(), endPos.getZ()),
Math.max(startPos.getX(), endPos.getX()) + 1,
Math.max(startPos.getY(), endPos.getY()) + 1,
Math.max(startPos.getZ(), endPos.getZ()) + 1
);
}
/**
* Assert all {@linkplain #checkInvariants() monitor invariants} hold.
*/
private void assertInvariant() {
assert checkInvariants() : "Monitor invariants failed. See logs.";
}
/**
* Check various invariants about this monitor multiblock. This is only called when assertions are enabled, so
* will be skipped outside of tests.
*
* @return Whether all invariants passed.
*/
private boolean checkInvariants() {
LOG.debug("Checking monitor invariants at {}", getBlockPos());
var okay = true;
if (width <= 0 || height <= 0) {
okay = false;
LOG.error("Monitor {} has non-positive of {}x{}", getBlockPos(), width, height);
}
var hasPeripheral = false;
var origin = getOrigin();
var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var monitor = getLoadedMonitor(x, y).getMonitor();
if (monitor == null) continue;
hasPeripheral |= monitor.peripheral != null;
if (monitor.serverMonitor != null && monitor.serverMonitor != serverMonitor) {
okay = false;
LOG.error(
"Monitor {} expected to be have serverMonitor={}, but was {}",
monitor.getBlockPos(), serverMonitor, monitor.serverMonitor
);
}
if (monitor.xIndex != x || monitor.yIndex != y) {
okay = false;
LOG.error(
"Monitor {} expected to be at {},{}, but believes it is {},{}",
monitor.getBlockPos(), x, y, monitor.xIndex, monitor.yIndex
);
}
if (monitor.width != width || monitor.height != height) {
okay = false;
LOG.error(
"Monitor {} expected to be size {},{}, but believes it is {},{}",
monitor.getBlockPos(), width, height, monitor.width, monitor.height
);
}
var expectedState = getBlockState().setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
y < height - 1, y > 0, x > 0, x < width - 1
));
if (monitor.getBlockState() != expectedState) {
okay = false;
LOG.error(
"Monitor {} expected to have state {}, but has state {}",
monitor.getBlockState(), expectedState, monitor.getBlockState()
);
}
}
}
if (hasPeripheral != (serverMonitor != null && serverMonitor.getTerminal() != null)) {
okay = false;
LOG.error(
"Peripheral is {}, but serverMonitor={} and serverMonitor.terminal={}",
hasPeripheral, serverMonitor, serverMonitor == null ? null : serverMonitor.getTerminal()
);
}
return okay;
}
}