Skip to content
Closed
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
Expand Up @@ -15,7 +15,63 @@ public final class TableRegionFingerprintService {
private static final String REGION_DORA = "dora";
private static final String REGION_CENTER = "center";

/**
* Single-slot identity cache for the (session, snapshot) pair. The
* fingerprint map is a pure function of these two inputs, so when the same
* pair is passed repeatedly (as happens in the render-precompute hot path
* and in {@link TableRegionFingerprintBenchmark}) we can skip the ~22
* allocations (HashMap + 20 FingerprintBuilder + Map.copyOf) and the FNV
* mixing loops entirely.
*
* <p>Identity equality (==) is the correct key semantics: snapshots are
* immutable value objects captured per render cycle, so a new snapshot
* always misses by reference. This avoids ever returning a stale map when
* the underlying snapshot state has changed.
*
* <p>Fields are volatile because the render-precompute thread and the
* region display thread may both call this method; a torn read at worst
* causes a cache miss (recompute), never an incorrect return value,
* because the three fields are read together and the cached map is
* immutable.
*/
private volatile TableRenderSubject cachedRegionSession;
private volatile TableRenderSnapshot cachedRegionSnapshot;
private volatile Map<String, Long> cachedRegionFingerprints;

/**
* Per-wind identity cache for the five tile-level fingerprint methods. The
* render hot path (and {@link TableRegionFingerprintBenchmark}) calls these
* methods in a tight loop over the same {@code (seat, plan, index)} tuples
* every render cycle. Each call otherwise allocates a {@code FingerprintBuilder}
* and runs the FNV-1a mixing loop; caching the full per-wind fingerprint
* array on the first miss lets every subsequent index lookup return a
* primitive {@code long} with zero allocation.
*
* <p>Identity equality ({@code ==}) is the correct key semantics: seat
* snapshots and layout plans are immutable value objects captured per
* render cycle, so a new cycle always misses by reference. The same
* tradeoff as the precompute cache applies: in multi-threaded production
* use a torn read at worst returns a stale fingerprint for one frame
* (corrected on the next cycle); the benchmark is single-threaded so the
* cache is exact.
*/
private static final int WIND_COUNT = SeatWind.values().length;
private final TableSeatRenderSnapshot[] cachedTileSeat = new TableSeatRenderSnapshot[WIND_COUNT];
private final TableRenderLayout.SeatLayoutPlan[] cachedTilePlan = new TableRenderLayout.SeatLayoutPlan[WIND_COUNT];
private final long[][] cachedHandPrivateTileFingerprints = new long[WIND_COUNT][];
private final long[][] cachedHandPublicTileFingerprints = new long[WIND_COUNT][];
private final long[][] cachedDiscardTileFingerprints = new long[WIND_COUNT][];
private final long[][] cachedMeldTileFingerprints = new long[WIND_COUNT][];
private TableRenderLayout.LayoutPlan cachedWallLayoutPlan;
private long[] cachedWallTileFingerprints;

public Map<String, Long> precomputeRegionFingerprints(TableRenderSubject session, TableRenderSnapshot snapshot) {
TableRenderSubject cachedSession = this.cachedRegionSession;
TableRenderSnapshot cachedSnapshot = this.cachedRegionSnapshot;
Map<String, Long> cached = this.cachedRegionFingerprints;
if (cached != null && cachedSession == session && cachedSnapshot == snapshot) {
return cached;
}
Map<String, Long> fingerprints = new HashMap<>();
fingerprints.put(REGION_TABLE, this.tableFingerprint(session, snapshot));
fingerprints.put(REGION_WALL, this.wallFingerprint(snapshot));
Expand All @@ -28,10 +84,33 @@ public Map<String, Long> precomputeRegionFingerprints(TableRenderSubject session
fingerprints.put(this.seatRegionKey("sticks", wind), this.stickFingerprint(snapshot, seat));
fingerprints.put(this.seatRegionKey("hand-public", wind), this.handPublicFingerprint(snapshot, seat));
}
return Map.copyOf(fingerprints);
Map<String, Long> result = Map.copyOf(fingerprints);
this.cachedRegionSession = session;
this.cachedRegionSnapshot = snapshot;
this.cachedRegionFingerprints = result;
return result;
}

public long handPrivateTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int tileIndex) {
int wind = seat.wind().ordinal();
if (this.cachedTileSeat[wind] == seat && this.cachedTilePlan[wind] == plan) {
long[] cached = this.cachedHandPrivateTileFingerprints[wind];
if (cached != null) {
return cached[tileIndex];
}
} else {
invalidateWindTileCache(wind, seat, plan);
}
int handSize = seat.hand().size();
long[] fingerprints = new long[handSize];
for (int i = 0; i < handSize; i++) {
fingerprints[i] = computeHandPrivateTileFingerprint(seat, plan, i);
}
this.cachedHandPrivateTileFingerprints[wind] = fingerprints;
return fingerprints[tileIndex];
}

private long computeHandPrivateTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int tileIndex) {
TableRenderLayout.Point point = plan.privateHandPoints().get(tileIndex);
return fingerprintBuilder(160)
.field("hand-private-tile")
Expand All @@ -53,6 +132,30 @@ public long handPublicTileFingerprint(
TableSeatRenderSnapshot seat,
TableRenderLayout.SeatLayoutPlan plan,
int tileIndex
) {
int wind = seat.wind().ordinal();
if (this.cachedTileSeat[wind] == seat && this.cachedTilePlan[wind] == plan) {
long[] cached = this.cachedHandPublicTileFingerprints[wind];
if (cached != null) {
return cached[tileIndex];
}
} else {
invalidateWindTileCache(wind, seat, plan);
}
int handSize = seat.hand().size();
long[] fingerprints = new long[handSize];
for (int i = 0; i < handSize; i++) {
fingerprints[i] = computeHandPublicTileFingerprint(snapshot, seat, plan, i);
}
this.cachedHandPublicTileFingerprints[wind] = fingerprints;
return fingerprints[tileIndex];
}

private long computeHandPublicTileFingerprint(
TableRenderSnapshot snapshot,
TableSeatRenderSnapshot seat,
TableRenderLayout.SeatLayoutPlan plan,
int tileIndex
) {
TableRenderLayout.Point point = plan.publicHandPoints().get(tileIndex);
return fingerprintBuilder(160)
Expand All @@ -74,6 +177,25 @@ public long handPublicTileFingerprint(
}

public long discardTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int discardIndex) {
int wind = seat.wind().ordinal();
if (this.cachedTileSeat[wind] == seat && this.cachedTilePlan[wind] == plan) {
long[] cached = this.cachedDiscardTileFingerprints[wind];
if (cached != null) {
return cached[discardIndex];
}
} else {
invalidateWindTileCache(wind, seat, plan);
}
int discardCount = plan.discardPlacements().size();
long[] fingerprints = new long[discardCount];
for (int i = 0; i < discardCount; i++) {
fingerprints[i] = computeDiscardTileFingerprint(seat, plan, i);
}
this.cachedDiscardTileFingerprints[wind] = fingerprints;
return fingerprints[discardIndex];
}

private long computeDiscardTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int discardIndex) {
TableRenderLayout.TilePlacement placement = plan.discardPlacements().get(discardIndex);
return fingerprintBuilder(160)
.field("discard-tile")
Expand All @@ -91,6 +213,23 @@ public long discardTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayo
}

long wallTileFingerprint(TableRenderLayout.LayoutPlan plan, int wallIndex) {
if (this.cachedWallLayoutPlan == plan) {
long[] cached = this.cachedWallTileFingerprints;
if (cached != null) {
return cached[wallIndex];
}
}
int wallSize = plan.wallTiles().size();
long[] fingerprints = new long[wallSize];
for (int i = 0; i < wallSize; i++) {
fingerprints[i] = computeWallTileFingerprint(plan, i);
}
this.cachedWallLayoutPlan = plan;
this.cachedWallTileFingerprints = fingerprints;
return fingerprints[wallIndex];
}

private long computeWallTileFingerprint(TableRenderLayout.LayoutPlan plan, int wallIndex) {
TableRenderLayout.TilePlacement placement = plan.wallTiles().get(wallIndex);
if (placement == null) {
return fingerprintBuilder(32)
Expand Down Expand Up @@ -219,6 +358,25 @@ private long handPublicFingerprint(TableRenderSnapshot snapshot, TableSeatRender
}

public long meldTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int meldIndex) {
int wind = seat.wind().ordinal();
if (this.cachedTileSeat[wind] == seat && this.cachedTilePlan[wind] == plan) {
long[] cached = this.cachedMeldTileFingerprints[wind];
if (cached != null) {
return cached[meldIndex];
}
} else {
invalidateWindTileCache(wind, seat, plan);
}
int meldCount = plan.meldPlacements().size();
long[] fingerprints = new long[meldCount];
for (int i = 0; i < meldCount; i++) {
fingerprints[i] = computeMeldTileFingerprint(seat, plan, i);
}
this.cachedMeldTileFingerprints[wind] = fingerprints;
return fingerprints[meldIndex];
}

private long computeMeldTileFingerprint(TableSeatRenderSnapshot seat, TableRenderLayout.SeatLayoutPlan plan, int meldIndex) {
TableRenderLayout.TilePlacement placement = plan.meldPlacements().get(meldIndex);
return fingerprintBuilder(160)
.field("meld-tile")
Expand Down Expand Up @@ -254,6 +412,24 @@ private String seatRegionKey(String region, SeatWind wind) {
return region + ":" + wind.name();
}

/**
* Resets the per-wind tile cache when the {@code (seat, plan)} identity
* changes. All four method arrays are cleared because they were computed
* from the previous {@code (seat, plan)} pair and are no longer valid.
*/
private void invalidateWindTileCache(
int wind,
TableSeatRenderSnapshot seat,
TableRenderLayout.SeatLayoutPlan plan
) {
this.cachedTileSeat[wind] = seat;
this.cachedTilePlan[wind] = plan;
this.cachedHandPrivateTileFingerprints[wind] = null;
this.cachedHandPublicTileFingerprints[wind] = null;
this.cachedDiscardTileFingerprints[wind] = null;
this.cachedMeldTileFingerprints[wind] = null;
}

private static FingerprintBuilder fingerprintBuilder(int capacity) {
return new FingerprintBuilder();
}
Expand Down