ref:dad8d800d3ce19b69e6c004d37f56fc36fcb056c

Performance: upload texture once per controller per frame, cache bounds

Before: N blocks in a group each uploaded the same 518K-pixel texture every frame → O(N * pixels) per frame. For a 3x2 grid, 6 redundant uploads of 518K pixels = 3.1M pixel writes per frame. After: frame-local HashSet tracks which controllers have been uploaded. First block in the group uploads, subsequent blocks skip. 1 upload per controller per frame regardless of group size. Also: - ScreenGroup pre-caches min/max X/Y/Z bounds in constructor (was creating 6 stream pipelines per getSubRegion call per frame) - Matrix rain animation updates every other tick instead of every tick - Terminal fills entire block face (no margin gap) 46 GameTests + 52 Rust tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SHA: dad8d800d3ce19b69e6c004d37f56fc36fcb056c
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-20 17:11
Parents: 8b9d5a7
2 files changed +69 -85
Type
common/src/main/java/io/fangorn/alacrittymc/block/ScreenGroup.java +25 −27
@@ -22,6 +22,9 @@
private final int gridCols; // blocks wide
private final int gridRows; // blocks tall
// Cached bounds for getSubRegion (avoid stream allocations per-frame)
private final int minX, maxX, minZ, maxZ, maxY;
private ScreenGroup(List<BlockPos> members, BlockPos controllerPos, Direction facing,
int gridCols, int gridRows) {
this.members = Collections.unmodifiableList(members);
@@ -29,6 +32,19 @@
this.facing = facing;
this.gridCols = gridCols;
this.gridRows = gridRows;
// Pre-compute bounds once
int mnX = Integer.MAX_VALUE, mxX = Integer.MIN_VALUE;
int mnZ = Integer.MAX_VALUE, mxZ = Integer.MIN_VALUE;
int mxY = Integer.MIN_VALUE;
for (BlockPos p : members) {
mnX = Math.min(mnX, p.getX()); mxX = Math.max(mxX, p.getX());
mnZ = Math.min(mnZ, p.getZ()); mxZ = Math.max(mxZ, p.getZ());
mxY = Math.max(mxY, p.getY());
}
this.minX = mnX; this.maxX = mxX;
this.minZ = mnZ; this.maxZ = mxZ;
this.maxY = mxY;
}
/**
@@ -126,36 +142,18 @@
* @return float[4]: {u0, v0, u1, v1} in range [0, 1], ready for direct use
*/
public float[] getSubRegion(BlockPos memberPos) {
// Uses pre-cached bounds (no stream allocations)
int blockCol, blockRow;
int blockCol;
int blockRow = this.maxY - memberPos.getY();
if (facing.getAxis() == Direction.Axis.Z) {
int minX = members.stream().mapToInt(BlockPos::getX).min().orElse(0);
int maxX = members.stream().mapToInt(BlockPos::getX).max().orElse(0);
int maxY = members.stream().mapToInt(BlockPos::getY).max().orElse(0);
blockRow = maxY - memberPos.getY();
if (facing == Direction.NORTH) {
// Viewer looks south: viewer's LEFT is +X
// Highest X → leftmost on screen → u starts at 0
blockCol = maxX - memberPos.getX();
} else {
blockCol = (facing == Direction.NORTH)
? this.maxX - memberPos.getX()
: memberPos.getX() - this.minX;
// SOUTH: viewer looks north: viewer's LEFT is -X
// Lowest X → leftmost on screen → u starts at 0
blockCol = memberPos.getX() - minX;
}
} else {
int minZ = members.stream().mapToInt(BlockPos::getZ).min().orElse(0);
int maxZ = members.stream().mapToInt(BlockPos::getZ).max().orElse(0);
int maxY = members.stream().mapToInt(BlockPos::getY).max().orElse(0);
blockRow = maxY - memberPos.getY();
if (facing == Direction.EAST) {
// Viewer looks west: viewer's LEFT is +Z
blockCol = maxZ - memberPos.getZ();
} else {
// WEST: viewer looks east: viewer's LEFT is -Z
blockCol = memberPos.getZ() - minZ;
}
blockCol = (facing == Direction.EAST)
? this.maxZ - memberPos.getZ()
: memberPos.getZ() - this.minZ;
}
float u0 = (float) blockCol / gridCols;