Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache the exposed area used for pathfinding and rendering FoW #3745

Merged
Merged
181 changes: 26 additions & 155 deletions src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,6 @@ public class ZoneRenderer extends JComponent
private Zone.Layer activeLayer;
private String loadingProgress;
private boolean isLoaded;
private BufferedImage fogBuffer;
/**
* I don't like this, at all, but it'll work for now, basically keep track of when the fog cache
* needs to be flushed in the case of switching views
*/
private boolean flushFog = true;

/** In screen space */
private Area exposedFogArea;
Expand Down Expand Up @@ -309,7 +303,6 @@ public void setZoneScale(Scale scale) {
evt -> {
if (Scale.PROPERTY_SCALE.equals(evt.getPropertyName())) {
tokenLocationCache.clear();
flushFog = true;
}
if (Scale.PROPERTY_OFFSET.equals(evt.getPropertyName())) {
// flushFog = true;
Expand Down Expand Up @@ -654,7 +647,7 @@ public void centerOn(CellPoint point) {
/**
* Remove the token from: tokenLocationCache, flipImageMap, opacityImageMap, replacementImageMap,
* labelRenderingCache. Set the visibleScreenArea, tokenStackMap, drawableLights, drawableAuras to
* null. Flush the fog. Flush the token from the zoneView.
* null. Flush the token from the zoneView.
*
* @param token the token to flush
*/
Expand All @@ -674,7 +667,6 @@ public void flush(Token token) {
// This could also be smarter
tokenStackMap = null;

flushFog = true;
drawableLights = null;
drawableAuras = null;

Expand All @@ -700,9 +692,9 @@ public void flush() {
flushDrawableRenderer();
flipImageMap.clear();
flipIsoImageMap.clear();
fogBuffer = null;
drawableLights = null;
drawableAuras = null;
zoneView.flushFog();

isLoaded = false;
}
Expand All @@ -717,7 +709,6 @@ public void flushLight() {

/** Set flushFog to true, visibleScreenArea to null, and repaints */
public void flushFog() {
flushFog = true;
visibleScreenArea = null;
repaintDebouncer.dispatch();
}
Expand Down Expand Up @@ -1076,11 +1067,9 @@ public Rectangle zoneExtents(PlayerView view) {

/**
* This method clears {@link #drawableAuras}, {@link #drawableLights}, {@link #visibleScreenArea},
* and {@link #lastView}. It also flushes the {@link #zoneView} and sets the {@link #flushFog}
* flag so that fog will be recalculated.
* and {@link #lastView}. It also flushes the {@link #zoneView}.
*/
public void invalidateCurrentViewCache() {
flushFog = true;
drawableLights = null;
drawableAuras = null;
visibleScreenArea = null;
Expand Down Expand Up @@ -1616,9 +1605,8 @@ private void renderPlayerVisionOverlay(Graphics2D g, PlayerView view) {
Area clip = new Area(new Rectangle(getSize().width, getSize().height));

Area viewArea = new Area(exposedFogArea);
List<Token> tokens = view.getTokens();
if (tokens != null && !tokens.isEmpty()) {
for (Token tok : tokens) {
if (view.isUsingTokenView()) {
for (Token tok : view.getTokens()) {
ExposedAreaMetaData exposedMeta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
viewArea.add(exposedMeta.getExposedAreaHistory());
}
Expand Down Expand Up @@ -1745,79 +1733,23 @@ private void renderLabels(Graphics2D g, PlayerView view) {
timer.stop("labels-1");
}

// Private cache variables just for renderFog() and no one else. :)
Integer fogX = null;
Integer fogY = null;

private Area renderFog(Graphics2D g, PlayerView view) {
private void renderFog(Graphics2D g, PlayerView view) {
Dimension size = getSize();
Area fogClip = new Area(new Rectangle(0, 0, size.width, size.height));
Area combined = null;

// Optimization for panning
if (!flushFog
&& fogX != null
&& fogY != null
&& (fogX != getViewOffsetX() || fogY != getViewOffsetY())) {
// This optimization does not seem to keep the alpha channel correctly, and sometimes leaves
// lines on some graphics boards, we'll leave it out for now
// if (Math.abs(fogX - getViewOffsetX()) < size.width && Math.abs(fogY - getViewOffsetY()) <
// size.height) {
// int deltaX = getViewOffsetX() - fogX;
// int deltaY = getViewOffsetY() - fogY;
//
// Graphics2D buffG = fogBuffer.createGraphics();
//
// buffG.setComposite(AlphaComposite.Src);
// buffG.copyArea(0, 0, size.width, size.height, deltaX, deltaY);
// buffG.dispose();
//
// fogClip = new Area();
// if (deltaX < 0) {
// fogClip.add(new Area(new Rectangle(size.width+deltaX, 0, -deltaX, size.height)));
// } else if (deltaX > 0){
// fogClip.add(new Area(new Rectangle(0, 0, deltaX, size.height)));
// }
//
// if (deltaY < 0) {
// fogClip.add(new Area(new Rectangle(0, size.height + deltaY, size.width, -deltaY)));
// } else if (deltaY > 0) {
// fogClip.add(new Area(new Rectangle(0, 0, size.width, deltaY)));
// }
// }
flushFog = true;
}
boolean cacheNotValid =
(fogBuffer == null
|| fogBuffer.getWidth() != size.width
|| fogBuffer.getHeight() != size.height);
timer.start("renderFog");
if (flushFog || cacheNotValid) {
fogX = getViewOffsetX();
fogY = getViewOffsetY();

boolean newImage = false;
if (cacheNotValid) {
newImage = true;
timer.start("renderFog-allocateBufferedImage");
fogBuffer = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB_PRE);
timer.stop("renderFog-allocateBufferedImage");
}
Graphics2D buffG = fogBuffer.createGraphics();

timer.start("renderFog-allocateBufferedImage");
try (final var entry = tempBufferPool.acquire()) {
final var buffer = entry.get();
timer.stop("renderFog-allocateBufferedImage");

timer.start("renderFog");
final var fogX = getViewOffsetX();
final var fogY = getViewOffsetY();

Graphics2D buffG = buffer.createGraphics();
buffG.setClip(fogClip);
SwingUtil.useAntiAliasing(buffG);

// XXX Is this even needed? Immediately below is another call to fillRect() with the same
// dimensions!
if (!newImage) {
timer.start("renderFog-clearOldImage");
// Composite oldComposite = buffG.getComposite();
buffG.setComposite(AlphaComposite.Clear);
// buffG.fillRect(0, 0, size.width, size.height); // Jamz: Removed as it's called again
// below
// buffG.setComposite(oldComposite);
timer.stop("renderFog-clearOldImage");
}
timer.start("renderFog-fill");
// Fill
double scale = getScale();
Expand Down Expand Up @@ -1848,84 +1780,23 @@ private Area renderFog(Graphics2D g, PlayerView view) {

String msg = null;
if (timer.isEnabled()) {
List<Token> list = view.getTokens();
msg = "renderFog-combined(" + (list == null ? 0 : list.size()) + ")";
msg = "renderFog-combined(" + (view.isUsingTokenView() ? view.getTokens().size() : 0) + ")";
}
timer.start(msg);
combined = zone.getExposedArea(view);
Area combined = zoneView.getExposedArea(view);
timer.stop(msg);

timer.start("renderFogArea");
Area exposedArea = null;
Area tempArea = new Area();
boolean combinedView =
!zoneView.isUsingVision()
|| MapTool.isPersonalServer()
|| !MapTool.getServerPolicy().isUseIndividualFOW()
|| view.isGMView();

if (view.getTokens() != null) {
// if there are tokens selected combine the areas, then, if individual FOW is enabled
// we pass the combined exposed area to build the soft FOW and visible area.
for (Token tok : view.getTokens()) {
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
exposedArea = meta.getExposedAreaHistory();
tempArea.add(new Area(exposedArea));
}
if (combinedView) {
// combined = zone.getExposedArea(view);
buffG.fill(combined);
renderFogArea(buffG, view, combined, visibleArea);
renderFogOutline(buffG, view, visibleArea);
} else {
// 'combined' already includes the area encompassed by 'tempArea', so just
// use 'combined' instead in this block of code?
tempArea.add(combined);
buffG.fill(tempArea);
renderFogArea(buffG, view, tempArea, visibleArea);
renderFogOutline(buffG, view, visibleArea);
}
} else {
// No tokens selected, so if we are using Individual FOW, we build up all the owned tokens
// exposed area's to build the soft FOW.
if (combinedView) {
if (combined.isEmpty()) {
combined = zone.getExposedArea();
}
buffG.fill(combined);
renderFogArea(buffG, view, combined, visibleArea);
renderFogOutline(buffG, view, visibleArea);
} else {
Area myCombined = new Area();
List<Token> myToks = zone.getTokens();
for (Token tok : myToks) {
if (!AppUtil.playerOwns(
tok)) { // Only here if !isGMview() so should the tokens already be in
// PlayerView.getTokens()?
continue;
}
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
exposedArea = meta.getExposedAreaHistory();
myCombined.add(new Area(exposedArea));
}
buffG.fill(myCombined);
renderFogArea(buffG, view, myCombined, visibleArea);
renderFogOutline(buffG, view, visibleArea);
}
}
// renderFogArea(buffG, view, combined, visibleArea);
buffG.fill(combined);
renderFogArea(buffG, view, combined, visibleArea);
renderFogOutline(buffG, view, visibleArea);
timer.stop("renderFogArea");

// timer.start("renderFogOutline");
// renderFogOutline(buffG, view, combined);
// timer.stop("renderFogOutline");

buffG.dispose();
flushFog = false;
timer.stop("renderFog");

g.drawImage(buffer, 0, 0, this);
}
timer.stop("renderFog");
g.drawImage(fogBuffer, 0, 0, this);
return combined;
}

private void renderFogArea(
Expand Down Expand Up @@ -4822,7 +4693,7 @@ public void modelChanged(ModelChangeEvent event) {
}
}
if (evt == Zone.Event.FOG_CHANGED) {
flushFog = true;
zoneView.flushFog();
}
if (evt == Zone.Event.DRAWABLE_ADDED || evt == Zone.Event.DRAWABLE_REMOVED) {
DrawnElement de = (DrawnElement) event.getArg();
Expand Down
50 changes: 48 additions & 2 deletions src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class ZoneView implements ModelChangeListener {
private final Map<LightSource.Type, Set<GUID>> lightSourceMap = new HashMap<>();
/** Map each token to their map between sightType and set of lights. */
private final Map<GUID, Map<String, Set<DrawableLight>>> drawableLightCache = new HashMap<>();
/** Map the PlayerView to its exposed area. */
private final Map<PlayerView, Area> exposedAreaMap = new HashMap<>();
/** Map the PlayerView to its visible area. */
private final Map<PlayerView, VisibleAreaMeta> visibleAreaMap = new HashMap<>();
/** Map each token to their personal drawable lights. */
Expand All @@ -70,6 +72,39 @@ public ZoneView(Zone zone) {
zone.addModelChangeListener(this);
}

public Area getExposedArea(PlayerView view) {
Area exposed = exposedAreaMap.get(view);

if (exposed == null) {
boolean combinedView =
!isUsingVision()
|| MapTool.isPersonalServer()
|| !MapTool.getServerPolicy().isUseIndividualFOW()
|| view.isGMView();

if (view.isUsingTokenView() || combinedView) {
exposed = zone.getExposedArea(view);
} else {
// Not a token-specific view, but we are using Individual FoW. So we build up all the owned
// tokens' exposed areas to build the soft FoW. Note that not all owned tokens may still
// have sight (so weren't included in the PlayerView), but could still have previously
// exposed areas.
exposed = new Area();
for (Token tok : zone.getTokens()) {
if (!AppUtil.playerOwns(tok)) {
continue;
}
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
Area exposedArea = meta.getExposedAreaHistory();
exposed.add(new Area(exposedArea));
}
}

exposedAreaMap.put(view, exposed);
}
return exposed;
}

/**
* Calculate the visible area of the view, cache it in visibleAreaMap, and return it
*
Expand Down Expand Up @@ -588,7 +623,7 @@ public Set<DrawableLight> getDrawableLights(PlayerView view) {
lightSet.addAll(set);
}
}
if (view != null && view.getTokens() != null) {
if (view != null && view.isUsingTokenView()) {
// Get the personal drawable lights of the tokens of the player view
for (Token token : view.getTokens()) {
Set<DrawableLight> lights = personalDrawableLightCache.get(token.getId());
Expand All @@ -608,11 +643,16 @@ public void flush() {
tokenVisibleAreaCache.clear();
tokenVisionCache.clear();
lightSourceCache.clear();
exposedAreaMap.clear();
visibleAreaMap.clear();
drawableLightCache.clear();
personalDrawableLightCache.clear();
}

public void flushFog() {
exposedAreaMap.clear();
}

/**
* Flush the ZoneView cache of the token. Remove token from tokenVisibleAreaCache,
* tokenVisionCache, lightSourceCache, drawableLightCache, and personal light caches. Can clear
Expand All @@ -632,8 +672,10 @@ public void flush(Token token) {
if (hadLightSource || token.hasLightSources()) {
// Have to recalculate all token vision
tokenVisionCache.clear();
exposedAreaMap.clear();
visibleAreaMap.clear();
} else if (token.getHasSight()) {
exposedAreaMap.clear();
visibleAreaMap.clear();
}
}
Expand Down Expand Up @@ -739,6 +781,7 @@ public void modelChanged(ModelChangeEvent event) {
lightSourceCache.clear();
drawableLightCache.clear();
personalDrawableLightCache.clear();
exposedAreaMap.clear();
visibleAreaMap.clear();
topologyAreas.clear();
topologyTrees.clear();
Expand Down Expand Up @@ -781,7 +824,10 @@ private boolean processTokenAddChangeEvent(List<Token> tokens) {
hasSight |= token.getHasSight();
}

if (hasSight) visibleAreaMap.clear();
if (hasSight) {
exposedAreaMap.clear();
visibleAreaMap.clear();
}

return hasTopology;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ protected List<CellPoint> calculatePath(CellPoint start, CellPoint goal) {

newFowExposedArea =
zoneRenderer.getZone().hasFog()
? zoneRenderer.getZone().getExposedArea(zoneRenderer.getPlayerView())
? zoneView.getExposedArea(zoneRenderer.getPlayerView())
: null;
}

Expand Down
Loading