Skip to content
Merged
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
117 changes: 117 additions & 0 deletions src/main/java/top/ellan/mahjong/render/layout/TableRenderLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Objects;

public final class TableRenderLayout {
private static final double ONE_SIXTEENTH = 1.0D / 16.0D;
Expand Down Expand Up @@ -76,6 +77,122 @@ public static LayoutPlan precompute(TableRenderSnapshot snapshot) {
);
}

/** Reuses immutable center, seat and wall geometry from the immediately preceding layout. */
public static LayoutPlan precompute(
TableRenderSnapshot snapshot,
TableRenderSnapshot previousSnapshot,
LayoutPlan previousLayout
) {
if (previousSnapshot == null || previousLayout == null) {
return precompute(snapshot);
}
boolean reusableCenter = sameCenter(snapshot, previousSnapshot);
Point displayCenter;
Point tableCenter;
Point tableVisualAnchor;
double borderSpanX;
double borderSpanZ;
if (reusableCenter) {
displayCenter = previousLayout.displayCenter();
tableCenter = previousLayout.tableCenter();
tableVisualAnchor = previousLayout.tableVisualAnchor();
borderSpanX = previousLayout.borderSpanX();
borderSpanZ = previousLayout.borderSpanZ();
} else {
displayCenter = new Point(snapshot.centerX(), snapshot.centerY() + DISPLAY_CENTER_Y_OFFSET, snapshot.centerZ());
TableBounds bounds = tableBoundsFromTiles(displayCenter);
tableCenter = new Point(bounds.centerX(), displayCenter.y(), bounds.centerZ());
tableVisualAnchor = new Point(tableCenter.x(), tableCenter.y() + TABLE_VISUAL_Y_OFFSET, tableCenter.z());
borderSpanX = bounds.width() + TABLE_TOP_SIZE_EXPANSION + TABLE_BORDER_THICKNESS;
borderSpanZ = bounds.depth() + TABLE_TOP_SIZE_EXPANSION + TABLE_BORDER_THICKNESS;
}

EnumMap<SeatWind, SeatLayoutPlan> seats = new EnumMap<>(SeatWind.class);
for (SeatWind wind : SEAT_WINDS) {
TableSeatRenderSnapshot seat = snapshot.seat(wind);
if (reusableCenter && sameSeatLayoutInputs(snapshot, previousSnapshot, wind)) {
seats.put(wind, previousLayout.seat(wind));
} else {
seats.put(wind, precomputeSeat(displayCenter, snapshot, seat));
}
}

List<TilePlacement> wallTiles;
List<TilePlacement> doraTiles;
if (reusableCenter && sameWallLayoutInputs(snapshot, previousSnapshot)) {
wallTiles = previousLayout.wallTiles();
doraTiles = previousLayout.doraTiles();
} else {
List<DeadWallPlacement> deadWallPlacements = snapshot.started() && snapshot.usesDeadWall()
? deadWallPlacements(displayCenter, snapshot)
: List.of();
wallTiles = precomputeWall(displayCenter, snapshot, deadWallPlacements);
doraTiles = precomputeDora(snapshot, deadWallPlacements);
}

return new LayoutPlan(
displayCenter,
tableCenter,
tableVisualAnchor,
borderSpanX,
borderSpanZ,
seats,
wallTiles,
doraTiles
);
}

private static boolean sameCenter(TableRenderSnapshot left, TableRenderSnapshot right) {
return Double.doubleToLongBits(left.centerX()) == Double.doubleToLongBits(right.centerX())
&& Double.doubleToLongBits(left.centerY()) == Double.doubleToLongBits(right.centerY())
&& Double.doubleToLongBits(left.centerZ()) == Double.doubleToLongBits(right.centerZ());
}

private static boolean sameSeatLayoutInputs(
TableRenderSnapshot leftSnapshot,
TableRenderSnapshot rightSnapshot,
SeatWind wind
) {
TableSeatRenderSnapshot left = leftSnapshot.seat(wind);
TableSeatRenderSnapshot right = rightSnapshot.seat(wind);
boolean leftOccupied = left.playerId() != null;
boolean rightOccupied = right.playerId() != null;
if (leftOccupied != rightOccupied) {
return false;
}
if (!leftOccupied) {
return true;
}
return left.hand().size() == right.hand().size()
&& left.selectedHandTileIndices().equals(right.selectedHandTileIndices())
&& left.riichiDiscardIndex() == right.riichiDiscardIndex()
&& left.stickLayoutCount() == right.stickLayoutCount()
&& left.discards().equals(right.discards())
&& left.melds().equals(right.melds())
&& left.cornerSticks().equals(right.cornerSticks())
&& left.riichi() == right.riichi()
&& leftSnapshot.openDoorSeat() == rightSnapshot.openDoorSeat();
}

private static boolean sameWallLayoutInputs(TableRenderSnapshot left, TableRenderSnapshot right) {
if (left.started() != right.started()
|| left.remainingWallCount() != right.remainingWallCount()
|| left.kanCount() != right.kanCount()
|| left.dicePoints() != right.dicePoints()
|| left.breakDicePoints() != right.breakDicePoints()
|| left.dealerSeat() != right.dealerSeat()
|| left.variant() != right.variant()
|| !left.doraIndicators().equals(right.doraIndicators())) {
return false;
}
for (SeatWind wind : SEAT_WINDS) {
if (!Objects.equals(left.seat(wind).melds(), right.seat(wind).melds())) {
return false;
}
}
return true;
}

public static SeatLayoutPlan precomputeSeatOnly(TableRenderSnapshot snapshot, SeatWind wind) {
if (snapshot == null || wind == null) {
throw new IllegalArgumentException("snapshot and wind are required");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public final class MahjongTableSession implements TableSessionMutator, TableMemb
private final TableRenderer renderer = new TableRenderer();
private final TableRenderSnapshotFactory renderSnapshotFactory = new TableRenderSnapshotFactory();
private final TableRegionFingerprintService regionFingerprintService = new TableRegionFingerprintService();
private final SessionRenderLayoutCache renderLayoutCache = new SessionRenderLayoutCache();
private MahjongRule configuredRule;
// These three fields are written on the table's region thread (during
// startRound / completeRoundStartInternal / setRoundControllerInternal)
Expand Down Expand Up @@ -1635,10 +1636,11 @@ public TableRenderSnapshot captureRenderSnapshot(long version, long cancellation
}

public TableRenderPrecomputeResult precomputeRender(TableRenderSnapshot snapshot) {
TableRenderLayout.LayoutPlan layout = this.renderLayoutCache.precompute(snapshot);
return new TableRenderPrecomputeResult(
snapshot,
this.regionFingerprintService.precomputeRegionFingerprints(this, snapshot),
TableRenderLayout.precompute(snapshot)
layout
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package top.ellan.mahjong.table.core;

import top.ellan.mahjong.model.MahjongTile;
import top.ellan.mahjong.model.MahjongVariant;
import top.ellan.mahjong.model.SeatWind;
import top.ellan.mahjong.render.layout.TableRenderLayout;
import top.ellan.mahjong.render.scene.MeldView;
import top.ellan.mahjong.render.snapshot.TableRenderSnapshot;
import top.ellan.mahjong.render.snapshot.TableSeatRenderSnapshot;
import top.ellan.mahjong.riichi.model.ScoringStick;
import java.util.List;

/** Single-entry, immutable layout cache owned by one table session. */
final class SessionRenderLayoutCache {
private volatile Entry cached;

TableRenderLayout.LayoutPlan precompute(TableRenderSnapshot snapshot) {
LayoutKey key = LayoutKey.from(snapshot);
Entry current = this.cached;
if (current != null && current.key().equals(key)) {
return current.layout();
}
TableRenderLayout.LayoutPlan layout = current == null
? TableRenderLayout.precompute(snapshot)
: TableRenderLayout.precompute(snapshot, current.snapshot(), current.layout());
this.cached = new Entry(key, snapshot, layout);
return layout;
}

void clear() {
this.cached = null;
}

private record Entry(LayoutKey key, TableRenderSnapshot snapshot, TableRenderLayout.LayoutPlan layout) {
}

private record LayoutKey(
double centerX,
double centerY,
double centerZ,
boolean started,
int remainingWallCount,
int kanCount,
int dicePoints,
int breakDicePoints,
SeatWind dealerSeat,
SeatWind openDoorSeat,
List<MahjongTile> doraIndicators,
MahjongVariant variant,
SeatLayoutKey east,
SeatLayoutKey south,
SeatLayoutKey west,
SeatLayoutKey north
) {
private static LayoutKey from(TableRenderSnapshot snapshot) {
return new LayoutKey(
snapshot.centerX(),
snapshot.centerY(),
snapshot.centerZ(),
snapshot.started(),
snapshot.remainingWallCount(),
snapshot.kanCount(),
snapshot.dicePoints(),
snapshot.breakDicePoints(),
snapshot.dealerSeat(),
snapshot.openDoorSeat(),
snapshot.doraIndicators(),
snapshot.variant(),
SeatLayoutKey.from(snapshot.seat(SeatWind.EAST)),
SeatLayoutKey.from(snapshot.seat(SeatWind.SOUTH)),
SeatLayoutKey.from(snapshot.seat(SeatWind.WEST)),
SeatLayoutKey.from(snapshot.seat(SeatWind.NORTH))
);
}
}

private record SeatLayoutKey(
SeatWind wind,
boolean occupied,
int handSize,
List<Integer> selectedHandTileIndices,
int riichiDiscardIndex,
int stickLayoutCount,
List<MahjongTile> discards,
List<MeldView> melds,
List<ScoringStick> cornerSticks,
boolean riichi
) {
private static SeatLayoutKey from(TableSeatRenderSnapshot seat) {
if (seat.playerId() == null) {
return new SeatLayoutKey(
seat.wind(), false, 0, List.of(), -1, 0, List.of(), List.of(), List.of(), false
);
}
return new SeatLayoutKey(
seat.wind(),
true,
seat.hand().size(),
seat.selectedHandTileIndices(),
seat.riichiDiscardIndex(),
seat.stickLayoutCount(),
seat.discards(),
seat.melds(),
seat.cornerSticks(),
seat.riichi()
);
}
}
}
Loading
Loading