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
6 changes: 6 additions & 0 deletions src/main/java/com/easywebmap/map/CompositeTileGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public CompletableFuture<byte[]> generateCompositeTile(String worldName, int zoo
int baseChunkX = tileX * chunksPerAxis;
int baseChunkZ = tileZ * chunksPerAxis;

// Early bail-out: if no chunks in this area are explored, return empty tile immediately
// This saves fetching 64+ base tiles for unexplored areas
if (!this.tileManager.hasAnyExploredChunks(worldName, baseChunkX, baseChunkZ, chunksPerAxis)) {
return CompletableFuture.completedFuture(PngEncoder.encodeEmpty(tileSize));
}

// Fetch all base tiles with pixels in parallel
List<CompletableFuture<TileWithPosition>> futures = new ArrayList<>();
for (int dz = 0; dz < chunksPerAxis; dz++) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/easywebmap/map/DiskTileCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class DiskTileCache {
public DiskTileCache(Path dataDirectory) {
this.cacheDirectory = dataDirectory.resolve("tilecache");
this.tileTimestamps = new ConcurrentHashMap<>();
this.diskExecutor = Executors.newFixedThreadPool(2, r -> {
// Increased from 2 to 6 threads for faster parallel disk reads during composite tile generation
this.diskExecutor = Executors.newFixedThreadPool(6, r -> {
Thread t = new Thread(r, "EasyWebMap-DiskIO");
t.setDaemon(true);
return t;
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/easywebmap/map/PngDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.easywebmap.map;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import javax.imageio.ImageIO;

/**
* Decodes PNG bytes back to raw pixel arrays for compositing.
* This is faster than regenerating tiles from WorldMapManager when
* tiles are already cached on disk.
*/
public class PngDecoder {

/**
* Decode PNG bytes to RGB pixel array.
*
* @param pngBytes The PNG image data
* @param expectedSize The expected width/height of the image
* @return RGB pixel array, or null if decoding fails
*/
public static int[] decode(byte[] pngBytes, int expectedSize) {
if (pngBytes == null || pngBytes.length == 0) {
return null;
}

try (ByteArrayInputStream bais = new ByteArrayInputStream(pngBytes)) {
BufferedImage image = ImageIO.read(bais);
if (image == null) {
return null;
}

int width = image.getWidth();
int height = image.getHeight();

// Ensure expected dimensions
if (width != expectedSize || height != expectedSize) {
// Scale if needed (shouldn't normally happen)
BufferedImage scaled = new BufferedImage(expectedSize, expectedSize, BufferedImage.TYPE_INT_RGB);
scaled.getGraphics().drawImage(image, 0, 0, expectedSize, expectedSize, null);
image = scaled;
width = expectedSize;
height = expectedSize;
}

// Extract pixels
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);

return pixels;
} catch (Exception e) {
return null;
}
}
}
82 changes: 79 additions & 3 deletions src/main/java/com/easywebmap/map/TileManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ public class TileManager {
private final ConcurrentHashMap<String, CachedChunkIndexes> chunkIndexCache;
private final ConcurrentHashMap<String, PngEncoder.TileData> pixelCache;
private CompositeTileGenerator compositeTileGenerator;
private static final int MAX_PIXEL_CACHE = 512;
// Increased from 512 to 2048 to prevent cache thrashing at negative zoom levels
// (A single zoom -4 tile requires 256 base tile pixels)
private static final int MAX_PIXEL_CACHE = 2048;
// Limit concurrent tile generations to prevent CPU spikes
private static final int MAX_CONCURRENT_GENERATIONS = 4;
// Increased from 4 to 16 for faster composite tile generation at negative zoom levels
private static final int MAX_CONCURRENT_GENERATIONS = 16;
private final Semaphore generationSemaphore = new Semaphore(MAX_CONCURRENT_GENERATIONS);
// Empty tiles are ~270 bytes, real tiles are 10KB+
private static final int EMPTY_TILE_THRESHOLD = 500;
Expand Down Expand Up @@ -178,6 +181,7 @@ public CompletableFuture<byte[]> getBaseTile(String worldName, int tileX, int ti
*/
public CompletableFuture<PngEncoder.TileData> getBaseTileWithPixels(String worldName, int tileX, int tileZ) {
String cacheKey = TileCache.createKey(worldName, 0, tileX, tileZ);
int tileSize = this.plugin.getConfig().getTileSize();

// 1. Check pixel cache
PngEncoder.TileData cached = this.pixelCache.get(cacheKey);
Expand All @@ -191,7 +195,27 @@ public CompletableFuture<PngEncoder.TileData> getBaseTileWithPixels(String world
return pending;
}

// 3. Generate with pixels
// 3. Check disk cache and decode PNG to pixels (faster than regenerating)
if (this.plugin.getConfig().isUseDiskCache()) {
byte[] diskCached = this.diskCache.get(worldName, 0, tileX, tileZ);
if (diskCached != null && diskCached.length > EMPTY_TILE_THRESHOLD) {
try {
int[] pixels = PngDecoder.decode(diskCached, tileSize);
if (pixels != null) {
PngEncoder.TileData tileData = new PngEncoder.TileData(diskCached, pixels, tileSize);
// Cache the pixels for future compositing
if (this.pixelCache.size() < MAX_PIXEL_CACHE) {
this.pixelCache.put(cacheKey, tileData);
}
return CompletableFuture.completedFuture(tileData);
}
} catch (Exception e) {
// Failed to decode, will regenerate
}
}
}

// 4. Generate with pixels
CompletableFuture<PngEncoder.TileData> future = this.generateTileWithPixels(worldName, tileX, tileZ);
this.pendingPixelRequests.put(cacheKey, future);
future.whenComplete((data, ex) -> {
Expand Down Expand Up @@ -357,6 +381,58 @@ private boolean isChunkExplored(World world, int chunkX, int chunkZ) {
}
}

/**
* Check if any chunks in the given area are explored.
* Used for early bail-out on composite tiles over unexplored areas.
*/
public boolean hasAnyExploredChunks(String worldName, int baseChunkX, int baseChunkZ, int chunksPerAxis) {
if (!this.plugin.getConfig().isRenderExploredChunksOnly()) {
return true; // If we're not filtering, assume there's content
}

World world = Universe.get().getWorld(worldName);
if (world == null) {
return false;
}

try {
LongSet indexes = this.getCachedChunkIndexes(world);
if (indexes == null) {
return true; // Fail open
}

// Quick sampling - check corners and center first
int[][] samplePoints = {
{0, 0}, // top-left
{chunksPerAxis - 1, 0}, // top-right
{0, chunksPerAxis - 1}, // bottom-left
{chunksPerAxis - 1, chunksPerAxis - 1}, // bottom-right
{chunksPerAxis / 2, chunksPerAxis / 2} // center
};

for (int[] point : samplePoints) {
long idx = ChunkUtil.indexChunk(baseChunkX + point[0], baseChunkZ + point[1]);
if (indexes.contains(idx)) {
return true;
}
}

// If samples show nothing, do a full scan (but this is rare)
for (int dz = 0; dz < chunksPerAxis; dz++) {
for (int dx = 0; dx < chunksPerAxis; dx++) {
long idx = ChunkUtil.indexChunk(baseChunkX + dx, baseChunkZ + dz);
if (indexes.contains(idx)) {
return true;
}
}
}

return false;
} catch (Exception e) {
return true; // Fail open
}
}

private LongSet getCachedChunkIndexes(World world) {
String worldName = world.getName();
CachedChunkIndexes cached = this.chunkIndexCache.get(worldName);
Expand Down
Loading