Skip to content

Commit cf85581

Browse files
committed
perf: improve map rendering speed
1 parent 27bc20b commit cf85581

4 files changed

Lines changed: 54 additions & 63 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55

66
group = "me.daoge.allaymap"
77
description = "AllayMap is a minimalistic and lightweight world map viewer for Allay servers, using the vanilla map rendering style"
8-
version = "0.1.2-SNAPSHOT"
8+
version = "0.2.0-SNAPSHOT"
99

1010
java {
1111
toolchain {

src/main/java/me/daoge/allaymap/AMEventListener.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ public class AMEventListener {
1919
private void onChunkLoad(ChunkLoadEvent event) {
2020
var chunk = event.getChunk();
2121
var dimension = event.getDimension();
22-
// Only mark dirty if this region hasn't been rendered yet
23-
// This avoids re-rendering when chunks are unloaded and reloaded
2422
renderQueue.markChunkDirty(dimension, chunk.getX(), chunk.getZ());
23+
// Shadow in z+1 chunk should be recalculated
24+
renderQueue.markChunkDirty(dimension, chunk.getX(), chunk.getZ() + 1);
2525
}
2626

2727
@EventHandler(priority = Integer.MIN_VALUE)
@@ -33,6 +33,8 @@ private void onBlockPlace(BlockPlaceEvent event) {
3333
var pos = event.getBlock().getPosition();
3434
var dimension = event.getBlock().getDimension();
3535
renderQueue.markBlockDirty(dimension, pos.x(), pos.z());
36+
// Shadow in z+1 chunk should be recalculated
37+
renderQueue.markBlockDirty(dimension, pos.x(), pos.z() + 1);
3638
}
3739

3840
@EventHandler(priority = Integer.MIN_VALUE)

src/main/java/me/daoge/allaymap/render/MapRenderer.java

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
import org.allaymc.api.world.biome.BiomeType;
1111
import org.allaymc.api.world.biome.BiomeTypes;
1212
import org.allaymc.api.world.chunk.Chunk;
13+
import org.allaymc.api.world.chunk.OperationType;
14+
import org.allaymc.api.world.chunk.UnsafeChunk;
1315

1416
import javax.imageio.ImageIO;
1517
import java.awt.*;
1618
import java.awt.image.BufferedImage;
19+
import java.util.Arrays;
1720
import java.util.Objects;
1821
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.atomic.AtomicReference;
1923

2024
/**
2125
* MapRenderer handles the rendering of chunks to images.
@@ -106,48 +110,41 @@ private static Color darker(Color source, double factor) {
106110
/**
107111
* Render a single chunk to a 16x16 tile image (1 pixel per block).
108112
*/
109-
public CompletableFuture<BufferedImage> renderChunk(Dimension dimension, int chunkX, int chunkZ) {
113+
public CompletableFuture<BufferedImage> renderChunk(Dimension dimension, Chunk chunk) {
110114
return CompletableFuture.supplyAsync(() -> {
111-
Chunk chunk = dimension.getChunkManager().getChunk(chunkX, chunkZ);
112-
if (chunk == null) {
113-
return null;
114-
}
115-
116-
int startX = chunkX << 4;
117-
int startZ = chunkZ << 4;
118115
int[] pixels = new int[CHUNK_SIZE * CHUNK_SIZE];
119116
int[] lastY = new int[CHUNK_SIZE];
120117

121118
// First pass: get initial heights for z=0
122-
for (int x = 0; x < CHUNK_SIZE; x++) {
123-
int worldX = startX + x;
124-
int worldZ = startZ - 1;
125-
HeightResult hr = getTopBlockHeight(dimension, worldX, worldZ);
126-
lastY[x] = hr != null ? hr.y : SEA_LEVEL;
119+
var lastChunk = dimension.getChunkManager().getChunk(chunk.getX(), chunk.getZ() - 1);
120+
if (lastChunk == null) {
121+
Arrays.fill(lastY, SEA_LEVEL);
122+
} else {
123+
lastChunk.applyOperation(unsafeChunk -> {
124+
for (int x = 0; x < CHUNK_SIZE; x++) {
125+
HeightResult hr = getTopBlockHeight(unsafeChunk, x, 15);
126+
lastY[x] = hr != null ? hr.y : SEA_LEVEL;
127+
}
128+
}, OperationType.READ, OperationType.NONE);
127129
}
128130

129-
// Main rendering pass
130-
for (int z = 0; z < CHUNK_SIZE; z++) {
131-
for (int x = 0; x < CHUNK_SIZE; x++) {
132-
int worldX = startX + x;
133-
int worldZ = startZ + z;
134-
135-
Color color = getMapColor(dimension, worldX, worldZ, lastY[x]);
136-
137-
HeightResult hr = getTopBlockHeight(dimension, worldX, worldZ);
138-
if (hr != null) {
139-
lastY[x] = hr.y;
131+
chunk.applyOperation(unsafeChunk -> {
132+
// Main rendering pass
133+
for (int z = 0; z < CHUNK_SIZE; z++) {
134+
for (int x = 0; x < CHUNK_SIZE; x++) {
135+
Color color = getMapColor(unsafeChunk, x, z, lastY[x]);
136+
HeightResult hr = getTopBlockHeight(unsafeChunk, x, z);
137+
lastY[x] = hr != null ? hr.y : SEA_LEVEL;
138+
pixels[z * CHUNK_SIZE + x] = color.getRGB();
140139
}
141-
142-
pixels[z * CHUNK_SIZE + x] = color.getRGB();
143140
}
144-
}
141+
}, OperationType.READ, OperationType.READ);
145142

146143
BufferedImage image = new BufferedImage(CHUNK_TILE_SIZE, CHUNK_TILE_SIZE, BufferedImage.TYPE_INT_ARGB);
147144
image.setRGB(0, 0, CHUNK_TILE_SIZE, CHUNK_TILE_SIZE, pixels, 0, CHUNK_TILE_SIZE);
148145
return image;
149146
}, Server.getInstance().getVirtualThreadPool()).exceptionally(e -> {
150-
AllayMap.getInstance().getPluginLogger().error("Error rendering chunk ({}, {})", chunkX, chunkZ, e);
147+
AllayMap.getInstance().getPluginLogger().error("Error rendering chunk ({}, {})", chunk.getX(), chunk.getZ(), e);
151148
return createEmptyChunkTile();
152149
});
153150
}
@@ -167,20 +164,20 @@ private BufferedImage createEmptyChunkTile() {
167164
/**
168165
* Get the map color for a specific position
169166
*/
170-
private Color getMapColor(Dimension dimension, int x, int z, int lastY) {
171-
HeightResult result = getTopBlockHeight(dimension, x, z);
167+
private Color getMapColor(UnsafeChunk chunk, int x, int z, int lastY) {
168+
var result = getTopBlockHeight(chunk, x, z);
172169
if (result == null) {
173170
return UNLOADED_CHUNK_COLOR;
174171
}
175172

176-
BlockState blockState = result.state;
173+
var blockState = result.state;
177174
int y = result.y;
178-
BiomeType biome = dimension.getBiome(x, y, z);
175+
var biome = chunk.getBiome(x, y, z);
179176

180-
Color color = computeMapColor(blockState, biome);
177+
var color = computeMapColor(blockState, biome);
181178

182179
// Check if block is underwater
183-
if (dimension.getBlockState(x, y + 1, z).getBlockType().hasBlockTag(BlockTags.WATER)) {
180+
if (chunk.getBlockState(x, y + 1, z).getBlockType().hasBlockTag(BlockTags.WATER)) {
184181
if (AllayMap.getInstance().getConfig().renderUnderwaterBlocks()) {
185182
color = applyWaterTint(color, y, biome);
186183
} else {
@@ -200,24 +197,14 @@ private Color getMapColor(Dimension dimension, int x, int z, int lastY) {
200197

201198
/**
202199
* Find the top renderable block at a position.
203-
* Only uses already-loaded chunks, does NOT trigger chunk loading.
204200
*/
205-
private HeightResult getTopBlockHeight(Dimension dimension, int x, int z) {
206-
// Only get the chunk if it's already loaded - do NOT load it
207-
Chunk chunk = dimension.getChunkManager().getChunk(x >> 4, z >> 4);
208-
if (chunk == null) {
209-
return null;
210-
}
211-
212-
int chunkX = x & 0xF;
213-
int chunkZ = z & 0xF;
214-
215-
var dimensionInfo = dimension.getDimensionInfo();
216-
int height = AllayMap.getInstance().getConfig().ignoreWorldHeightMap() ? dimensionInfo.maxHeight() : chunk.getHeight(chunkX, chunkZ);
201+
private HeightResult getTopBlockHeight(UnsafeChunk chunk, int x, int z) {
202+
var dimensionInfo = chunk.getDimensionInfo();
217203
int minHeight = dimensionInfo.minHeight();
218204

205+
int height = AllayMap.getInstance().getConfig().ignoreWorldHeightMap() ? dimensionInfo.maxHeight() : chunk.getHeight(x, z);
219206
while (height >= minHeight) {
220-
BlockState state = chunk.getBlockState(chunkX, height, chunkZ);
207+
BlockState state = chunk.getBlockState(x, height, z);
221208
var mapColor = state.getBlockStateData().mapColor();
222209
var tintMethod = state.getBlockStateData().tintMethod();
223210

src/main/java/me/daoge/allaymap/render/MapTileManager.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.allaymc.api.server.Server;
66
import org.allaymc.api.utils.hash.HashUtils;
77
import org.allaymc.api.world.Dimension;
8+
import org.allaymc.api.world.chunk.Chunk;
89
import org.slf4j.Logger;
910

1011
import javax.imageio.ImageIO;
@@ -77,11 +78,11 @@ private void processDirtyChunks() {
7778
logger.debug("Processing {} dirty chunks in {}", dirtyChunks.size(), world.getName());
7879

7980
for (long chunkKey : dirtyChunks) {
80-
int chunkX = HashUtils.getXFromHashXZ(chunkKey);
81-
int chunkZ = HashUtils.getZFromHashXZ(chunkKey);
82-
83-
// Render and save the chunk tile (zoom 0)
84-
renderAndSaveChunk(dimension, chunkX, chunkZ);
81+
var chunk = dimension.getChunkManager().getChunk(chunkKey);
82+
if (chunk != null) {
83+
// Render and save the chunk tile (zoom 0)
84+
renderAndSaveChunk(dimension, chunk);
85+
}
8586
}
8687
}
8788
}
@@ -90,22 +91,23 @@ private void processDirtyChunks() {
9091
/**
9192
* Render a chunk and save it to disk.
9293
*/
93-
private void renderAndSaveChunk(Dimension dimension, int chunkX, int chunkZ) {
94-
String dimensionName = getDimensionName(dimension);
95-
String key = "render:" + getTilePath(dimensionName, chunkX, chunkZ);
94+
private void renderAndSaveChunk(Dimension dimension, Chunk chunk) {
95+
var dimensionName = getDimensionName(dimension);
96+
var key = "render:" + getTilePath(dimensionName, chunk.getX(), chunk.getZ());
9697

97-
tileTasks.computeIfAbsent(key, k -> renderer.renderChunk(dimension, chunkX, chunkZ)
98+
tileTasks.computeIfAbsent(key, k -> renderer.renderChunk(dimension, chunk)
9899
.thenApply(image -> {
99100
if (image != null) {
100-
saveTile(dimensionName, chunkX, chunkZ, image);
101+
// Only save the image if it is generated successfully
102+
saveTile(dimensionName, chunk.getX(), chunk.getZ(), image);
101103
return image;
102104
}
103105

104106
return createEmptyTile();
105107
})
106108
.exceptionally(e -> {
107-
logger.error("Failed to render chunk ({}, {})", chunkX, chunkZ, e);
108-
return null;
109+
logger.error("Failed to render chunk ({}, {})", chunk.getX(), chunk.getZ(), e);
110+
return createEmptyTile();
109111
})
110112
).whenComplete((result, error) -> tileTasks.remove(key));
111113
}

0 commit comments

Comments
 (0)