Merge pull request #156 from mikeprimm/master

Implement chunk snapshotting approach to drop memory use and CPU use significantly, add shadows option (via shadowstrength) on defaulttilerender, add fix for nicknamed players' skins
This commit is contained in:
mikeprimm 2011-05-20 23:03:35 -07:00
commit cb010802db
13 changed files with 540 additions and 179 deletions

View File

@ -59,6 +59,8 @@ worlds:
prefix: t prefix: t
maximumheight: 127 maximumheight: 127
colorscheme: default colorscheme: default
# Add shadows to world (based on top-down shadows from chunk data)
# shadowstrength: 1.0
#- class: org.dynmap.kzedmap.HighlightTileRenderer #- class: org.dynmap.kzedmap.HighlightTileRenderer
# prefix: ht # prefix: ht
# maximumheight: 127 # maximumheight: 127

View File

@ -0,0 +1,106 @@
package org.dynmap;
/**
* Represents a static, thread-safe snapshot of chunk of blocks
* Purpose is to allow clean, efficient copy of a chunk data to be made, and then handed off for processing in another thread (e.g. map rendering)
*/
public class CraftChunkSnapshot {
private final int x, z;
private final byte[] buf; /* Flat buffer in uncompressed chunk file format */
private static final int BLOCKDATA_OFF = 32768;
private static final int BLOCKLIGHT_OFF = BLOCKDATA_OFF + 16384;
private static final int SKYLIGHT_OFF = BLOCKLIGHT_OFF + 16384;
/**
* Constructor
*/
CraftChunkSnapshot(int x, int z, byte[] buf) {
this.x = x;
this.z = z;
this.buf = buf;
}
/**
* Gets the X-coordinate of this chunk
*
* @return X-coordinate
*/
public int getX() {
return x;
}
/**
* Gets the Z-coordinate of this chunk
*
* @return Z-coordinate
*/
public int getZ() {
return z;
}
/**
* Get block type for block at corresponding coordinate in the chunk
*
* @param x 0-15
* @param y 0-127
* @param z 0-15
* @return 0-255
*/
public int getBlockTypeId(int x, int y, int z) {
return buf[x << 11 | z << 7 | y] & 255;
}
/**
* Get block data for block at corresponding coordinate in the chunk
*
* @param x 0-15
* @param y 0-127
* @param z 0-15
* @return 0-15
*/
public int getBlockData(int x, int y, int z) {
int off = ((x << 10) | (z << 6) | (y >> 1)) + BLOCKDATA_OFF;
return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF);
}
/**
* Get sky light level for block at corresponding coordinate in the chunk
*
* @param x 0-15
* @param y 0-127
* @param z 0-15
* @return 0-15
*/
public int getBlockSkyLight(int x, int y, int z) {
int off = ((x << 10) | (z << 6) | (y >> 1)) + SKYLIGHT_OFF;
return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF);
}
/**
* Get light level emitted by block at corresponding coordinate in the chunk
*
* @param x 0-15
* @param y 0-127
* @param z 0-15
* @return 0-15
*/
public int getBlockEmittedLight(int x, int y, int z) {
int off = ((x << 10) | (z << 6) | (y >> 1)) + BLOCKLIGHT_OFF;
return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF);
}
public int getHighestBlockYAt(int x, int z) {
int off = x << 11 | z << 7 | 126;
int i;
for(i = 127; (i >= 2); i--, off--) {
if(buf[off] != 0) {
break;
}
}
return i;
}
}

View File

@ -0,0 +1,213 @@
package org.dynmap;
import java.lang.reflect.Method;
import java.util.LinkedList;
import org.bukkit.World;
import org.bukkit.Chunk;
import org.bukkit.entity.Entity;
/**
* Container for managing chunks, as well as abstracting the different methods we may
* handle chunk data (existing chunk loading, versus upcoming chunk snapshots)
*
*/
public class MapChunkCache {
private World w;
private static Method getchunkdata = null;
private static Method gethandle = null;
private static boolean initialized = false;
private int x_min, x_max, z_min, z_max;
private int x_dim;
private CraftChunkSnapshot[] snaparray; /* Index = (x-x_min) + ((z-z_min)*x_dim) */
private LinkedList<DynmapChunk> loadedChunks = new LinkedList<DynmapChunk>();
/**
* Create chunk cache container
* @param w - world
* @param x_min - minimum chunk x coordinate
* @param z_min - minimum chunk z coordinate
* @param x_max - maximum chunk x coordinate
* @param z_max - maximum chunk z coordinate
*/
@SuppressWarnings({ "unchecked" })
public MapChunkCache(World w, DynmapChunk[] chunks) {
/* Compute range */
if(chunks.length == 0) {
this.x_min = 0;
this.x_max = 0;
this.z_min = 0;
this.z_max = 0;
x_dim = 1;
}
else {
x_min = x_max = chunks[0].x;
z_min = z_max = chunks[0].z;
for(int i = 1; i < chunks.length; i++) {
if(chunks[i].x > x_max)
x_max = chunks[i].x;
if(chunks[i].x < x_min)
x_min = chunks[i].x;
if(chunks[i].z > z_max)
z_max = chunks[i].z;
if(chunks[i].z < z_min)
z_min = chunks[i].z;
}
x_dim = x_max - x_min + 1;
}
this.w = w;
if(!initialized) {
try {
Class c = Class.forName("net.minecraft.server.Chunk");
getchunkdata = c.getDeclaredMethod("a", new Class[] { byte[].class, int.class,
int.class, int.class, int.class, int.class, int.class, int.class });
c = Class.forName("org.bukkit.craftbukkit.CraftChunk");
gethandle = c.getDeclaredMethod("getHandle", new Class[0]);
} catch (ClassNotFoundException cnfx) {
} catch (NoSuchMethodException nsmx) {
}
initialized = true;
if(gethandle != null)
Log.info("Chunk snapshot support enabled");
else
Log.info("Chunk snapshot support disabled");
}
if(gethandle != null) { /* We can use caching */
snaparray = new CraftChunkSnapshot[x_dim * (z_max-z_min+1)];
}
if(snaparray != null) {
// Load the required chunks.
for (DynmapChunk chunk : chunks) {
boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z);
boolean didload = w.loadChunk(chunk.x, chunk.z, false);
/* If it did load, make cache of it */
if(didload) {
Chunk c = w.getChunkAt(chunk.x, chunk.z);
try {
Object cc = gethandle.invoke(c);
byte[] buf = new byte[32768 + 16384 + 16384 + 16384]; /* Get big enough buffer for whole chunk */
getchunkdata.invoke(cc, buf, 0, 0, 0, 16, 128, 16, 0);
CraftChunkSnapshot ss = new CraftChunkSnapshot(chunk.x, chunk.z, buf);
snaparray[(chunk.x-x_min) + (chunk.z - z_min)*x_dim] = ss;
} catch (Exception x) {
}
}
if ((!wasLoaded) && didload) {
/* It looks like bukkit "leaks" entities - they don't get removed from the world-level table
* when chunks are unloaded but not saved - removing them seems to do the trick */
Chunk cc = w.getChunkAt(chunk.x, chunk.z);
if(cc != null) {
for(Entity e: cc.getEntities())
e.remove();
}
/* Since we only remember ones we loaded, and we're synchronous, no player has
* moved, so it must be safe (also prevent chunk leak, which appears to happen
* because isChunkInUse defined "in use" as being within 256 blocks of a player,
* while the actual in-use chunk area for a player where the chunks are managed
* by the MC base server is 21x21 (or about a 160 block radius) */
w.unloadChunk(chunk.x, chunk.z, false, false);
}
}
}
else { /* Else, load and keep them loaded for now */
// Load the required chunks.
for (DynmapChunk chunk : chunks) {
boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z);
boolean didload = w.loadChunk(chunk.x, chunk.z, false);
if ((!wasLoaded) && didload)
loadedChunks.add(chunk);
}
}
}
/**
* Unload chunks
*/
public void unloadChunks() {
if(snaparray != null) {
for(int i = 0; i < snaparray.length; i++) {
snaparray[i] = null;
}
}
else {
while (!loadedChunks.isEmpty()) {
DynmapChunk c = loadedChunks.pollFirst();
/* It looks like bukkit "leaks" entities - they don't get removed from the world-level table
* when chunks are unloaded but not saved - removing them seems to do the trick */
Chunk cc = w.getChunkAt(c.x, c.z);
if(cc != null) {
for(Entity e: cc.getEntities())
e.remove();
}
/* Since we only remember ones we loaded, and we're synchronous, no player has
* moved, so it must be safe (also prevent chunk leak, which appears to happen
* because isChunkInUse defined "in use" as being within 256 blocks of a player,
* while the actual in-use chunk area for a player where the chunks are managed
* by the MC base server is 21x21 (or about a 160 block radius) */
w.unloadChunk(c.x, c.z, false, false);
}
}
}
/**
* Get block ID at coordinates
*/
public int getBlockTypeID(int x, int y, int z) {
if(snaparray != null) {
CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim];
if(ss == null)
return 0;
else
return ss.getBlockTypeId(x & 0xF, y, z & 0xF);
}
else {
return w.getBlockTypeIdAt(x, y, z);
}
}
/**
* Get block data at coordiates
*/
public byte getBlockData(int x, int y, int z) {
if(snaparray != null) {
CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim];
if(ss == null)
return 0;
else
return (byte)ss.getBlockData(x & 0xF, y, z & 0xF);
}
else {
return w.getBlockAt(x, y, z).getData();
}
}
/* Get highest block Y
*
*/
public int getHighestBlockYAt(int x, int z) {
if(snaparray != null) {
CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim];
if(ss == null) {
return 1;
}
else
return ss.getHighestBlockYAt(x & 0xF, z & 0xF);
}
else {
return w.getHighestBlockYAt(x, z);
}
}
/* Get sky light level
*/
public int getBlockSkyLight(int x, int y, int z) {
if(snaparray != null) {
CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim];
if(ss == null) {
return 15;
}
else
return ss.getBlockSkyLight(x & 0xF, y, z & 0xF);
}
else {
return 15;
}
}
}

View File

@ -21,9 +21,7 @@ public class MapManager {
public Map<String, DynmapWorld> inactiveworlds = new HashMap<String, DynmapWorld>(); public Map<String, DynmapWorld> inactiveworlds = new HashMap<String, DynmapWorld>();
private BukkitScheduler scheduler; private BukkitScheduler scheduler;
private DynmapPlugin plug_in; private DynmapPlugin plug_in;
private boolean do_timesliced_render = false;
private double timeslice_interval = 0.0; private double timeslice_interval = 0.0;
private boolean do_sync_render = false; /* Do incremental renders on sync thread too */
/* Which timesliced renders are active */ /* Which timesliced renders are active */
private HashMap<String, FullWorldRenderState> active_renders = new HashMap<String, FullWorldRenderState>(); private HashMap<String, FullWorldRenderState> active_renders = new HashMap<String, FullWorldRenderState>();
@ -96,22 +94,14 @@ public class MapManager {
else { /* Else, single tile render */ else { /* Else, single tile render */
tile = tile0; tile = tile0;
} }
DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile);
LinkedList<DynmapChunk> loadedChunks = new LinkedList<DynmapChunk>(); MapChunkCache cache = new MapChunkCache(world.world, requiredChunks);
World w = world.world; World w = world.world;
// Load the required chunks.
for (DynmapChunk chunk : requiredChunks) {
boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z);
boolean didload = w.loadChunk(chunk.x, chunk.z, false);
if ((!wasLoaded) && didload)
loadedChunks.add(chunk);
}
if(tile0 != null) { /* Single tile? */ if(tile0 != null) { /* Single tile? */
render(tile); /* Just render */ render(cache, tile); /* Just render */
} }
else { else {
if (render(tile)) { if (render(cache, tile)) {
found.remove(tile); found.remove(tile);
rendered.add(tile); rendered.add(tile);
for (MapTile adjTile : map.getAdjecentTiles(tile)) { for (MapTile adjTile : map.getAdjecentTiles(tile)) {
@ -129,22 +119,7 @@ public class MapManager {
} }
} }
/* And unload what we loaded */ /* And unload what we loaded */
while (!loadedChunks.isEmpty()) { cache.unloadChunks();
DynmapChunk c = loadedChunks.pollFirst();
/* It looks like bukkit "leaks" entities - they don't get removed from the world-level table
* when chunks are unloaded but not saved - removing them seems to do the trick */
Chunk cc = w.getChunkAt(c.x, c.z);
if(cc != null) {
for(Entity e: cc.getEntities())
e.remove();
}
/* Since we only remember ones we loaded, and we're synchronous, no player has
* moved, so it must be safe (also prevent chunk leak, which appears to happen
* because isChunkInUse defined "in use" as being within 256 blocks of a player,
* while the actual in-use chunk area for a player where the chunks are managed
* by the MC base server is 21x21 (or about a 160 block radius) */
w.unloadChunk(c.x, c.z, false, false);
}
if(tile0 == null) { /* fullrender */ if(tile0 == null) { /* fullrender */
/* Schedule the next tile to be worked */ /* Schedule the next tile to be worked */
scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20));
@ -159,11 +134,8 @@ public class MapManager {
this.tileQueue = new AsynchronousQueue<MapTile>(new Handler<MapTile>() { this.tileQueue = new AsynchronousQueue<MapTile>(new Handler<MapTile>() {
@Override @Override
public void handle(MapTile t) { public void handle(MapTile t) {
if(do_sync_render) scheduler.scheduleSyncDelayedTask(plug_in,
scheduler.scheduleSyncDelayedTask(plug_in, new FullWorldRenderState(t), 1);
new FullWorldRenderState(t), 1);
else
render(t);
} }
}, (int) (configuration.getDouble("renderinterval", 0.5) * 1000)); }, (int) (configuration.getDouble("renderinterval", 0.5) * 1000));
@ -175,9 +147,7 @@ public class MapManager {
} }
}, 10); }, 10);
do_timesliced_render = configuration.getBoolean("timeslicerender", true);
timeslice_interval = configuration.getDouble("timesliceinterval", 0.5); timeslice_interval = configuration.getDouble("timesliceinterval", 0.5);
do_sync_render = configuration.getBoolean("renderonsync", true);
for(ConfigurationNode worldConfiguration : configuration.getNodes("worlds")) { for(ConfigurationNode worldConfiguration : configuration.getNodes("worlds")) {
String worldName = worldConfiguration.getString("name"); String worldName = worldConfiguration.getString("name");
@ -219,78 +189,17 @@ public class MapManager {
Log.severe("Could not render: world '" + l.getWorld().getName() + "' not defined in configuration."); Log.severe("Could not render: world '" + l.getWorld().getName() + "' not defined in configuration.");
return; return;
} }
if(do_timesliced_render) { String wname = l.getWorld().getName();
String wname = l.getWorld().getName(); FullWorldRenderState rndr = active_renders.get(wname);
FullWorldRenderState rndr = active_renders.get(wname); if(rndr != null) {
if(rndr != null) { Log.info("Full world render of world '" + wname + "' already active.");
Log.info("Full world render of world '" + wname + "' already active.");
return;
}
rndr = new FullWorldRenderState(world,l); /* Make new activation record */
active_renders.put(wname, rndr); /* Add to active table */
/* Schedule first tile to be worked */
scheduler.scheduleSyncDelayedTask(plug_in, rndr, (int)(timeslice_interval*20));
Log.info("Full render starting on world '" + wname + "' (timesliced)...");
return; return;
} }
World w = world.world; rndr = new FullWorldRenderState(world,l); /* Make new activation record */
active_renders.put(wname, rndr); /* Add to active table */
Log.info("Full render starting on world '" + w.getName() + "'..."); /* Schedule first tile to be worked */
for (MapType map : world.maps) { scheduler.scheduleSyncDelayedTask(plug_in, rndr, (int)(timeslice_interval*20));
int requiredChunkCount = 200; Log.info("Full render starting on world '" + wname + "' (timesliced)...");
HashSet<MapTile> found = new HashSet<MapTile>();
HashSet<MapTile> rendered = new HashSet<MapTile>();
LinkedList<MapTile> renderQueue = new LinkedList<MapTile>();
LinkedList<DynmapChunk> loadedChunks = new LinkedList<DynmapChunk>();
for (MapTile tile : map.getTiles(l)) {
if (!found.contains(tile)) {
found.add(tile);
renderQueue.add(tile);
}
}
while (!renderQueue.isEmpty()) {
MapTile tile = renderQueue.pollFirst();
DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile);
if (requiredChunks.length > requiredChunkCount)
requiredChunkCount = requiredChunks.length;
// Unload old chunks.
while (loadedChunks.size() >= requiredChunkCount - requiredChunks.length) {
DynmapChunk c = loadedChunks.pollFirst();
w.unloadChunk(c.x, c.z, false, true);
}
// Load the required chunks.
for (DynmapChunk chunk : requiredChunks) {
boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z);
w.loadChunk(chunk.x, chunk.z, false);
if (!wasLoaded)
loadedChunks.add(chunk);
}
if (render(tile)) {
found.remove(tile);
rendered.add(tile);
for (MapTile adjTile : map.getAdjecentTiles(tile)) {
if (!found.contains(adjTile) && !rendered.contains(adjTile)) {
found.add(adjTile);
renderQueue.add(adjTile);
}
}
}
found.remove(tile);
}
// Unload remaining chunks to clean-up.
while (!loadedChunks.isEmpty()) {
DynmapChunk c = loadedChunks.pollFirst();
w.unloadChunk(c.x, c.z, false, true);
}
}
Log.info("Full render finished.");
} }
public void activateWorld(World w) { public void activateWorld(World w) {
@ -337,8 +246,8 @@ public class MapManager {
writeQueue.stop(); writeQueue.stop();
} }
public boolean render(MapTile tile) { public boolean render(MapChunkCache cache, MapTile tile) {
boolean result = tile.getMap().render(tile, getTileFile(tile)); boolean result = tile.getMap().render(cache, tile, getTileFile(tile));
//Do update after async file write //Do update after async file write
return result; return result;
@ -387,6 +296,6 @@ public class MapManager {
} }
public boolean doSyncRender() { public boolean doSyncRender() {
return do_sync_render; return true;
} }
} }

View File

@ -13,5 +13,5 @@ public abstract class MapType {
public abstract DynmapChunk[] getRequiredChunks(MapTile tile); public abstract DynmapChunk[] getRequiredChunks(MapTile tile);
public abstract boolean render(MapTile tile, File outputFile); public abstract boolean render(MapChunkCache cache, MapTile tile, File outputFile);
} }

View File

@ -19,6 +19,8 @@ import org.dynmap.MapManager;
import org.dynmap.MapTile; import org.dynmap.MapTile;
import org.dynmap.MapType; import org.dynmap.MapType;
import org.dynmap.debug.Debug; import org.dynmap.debug.Debug;
import org.dynmap.kzedmap.KzedMap;
import org.dynmap.MapChunkCache;
public class FlatMap extends MapType { public class FlatMap extends MapType {
private String prefix; private String prefix;
@ -73,13 +75,13 @@ public class FlatMap extends MapType {
} }
@Override @Override
public boolean render(MapTile tile, File outputFile) { public boolean render(MapChunkCache cache, MapTile tile, File outputFile) {
FlatMapTile t = (FlatMapTile) tile; FlatMapTile t = (FlatMapTile) tile;
World w = t.getWorld(); World w = t.getWorld();
boolean isnether = (w.getEnvironment() == Environment.NETHER) && (maximumHeight == 127); boolean isnether = (w.getEnvironment() == Environment.NETHER) && (maximumHeight == 127);
boolean rendered = false; boolean rendered = false;
BufferedImage im = new BufferedImage(t.size, t.size, BufferedImage.TYPE_INT_RGB); BufferedImage im = KzedMap.allocateBufferedImage(t.size, t.size);
WritableRaster raster = im.getRaster(); WritableRaster raster = im.getRaster();
int[] pixel = new int[4]; int[] pixel = new int[4];
@ -93,16 +95,16 @@ public class FlatMap extends MapType {
if(isnether) { if(isnether) {
/* Scan until we hit air */ /* Scan until we hit air */
my = 127; my = 127;
while((blockType = w.getBlockTypeIdAt(mx, my, mz)) != 0) { while((blockType = cache.getBlockTypeID(mx, my, mz)) != 0) {
my--; my--;
if(my < 0) { /* Solid - use top */ if(my < 0) { /* Solid - use top */
my = 127; my = 127;
blockType = w.getBlockTypeIdAt(mx, my, mz); blockType = cache.getBlockTypeID(mx, my, mz);
break; break;
} }
} }
if(blockType == 0) { /* Hit air - now find non-air */ if(blockType == 0) { /* Hit air - now find non-air */
while((blockType = w.getBlockTypeIdAt(mx, my, mz)) == 0) { while((blockType = cache.getBlockTypeID(mx, my, mz)) == 0) {
my--; my--;
if(my < 0) { if(my < 0) {
my = 0; my = 0;
@ -112,14 +114,14 @@ public class FlatMap extends MapType {
} }
} }
else { else {
my = w.getHighestBlockYAt(mx, mz) - 1; my = cache.getHighestBlockYAt(mx, mz) - 1;
if(my > maximumHeight) my = maximumHeight; if(my > maximumHeight) my = maximumHeight;
blockType = w.getBlockTypeIdAt(mx, my, mz); blockType = cache.getBlockTypeID(mx, my, mz);
} }
byte data = 0; byte data = 0;
Color[] colors = colorScheme.colors[blockType]; Color[] colors = colorScheme.colors[blockType];
if(colorScheme.datacolors[blockType] != null) { if(colorScheme.datacolors[blockType] != null) {
data = w.getBlockAt(mx, my, mz).getData(); data = cache.getBlockData(mx, my, mz);
colors = colorScheme.datacolors[blockType][data]; colors = colorScheme.datacolors[blockType][data];
} }
if (colors == null) if (colors == null)
@ -176,7 +178,7 @@ public class FlatMap extends MapType {
} catch (java.lang.NullPointerException e) { } catch (java.lang.NullPointerException e) {
Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e);
} }
img.flush(); KzedMap.freeBufferedImage(img);
MapManager.mapman.pushUpdate(mtile.getWorld(), MapManager.mapman.pushUpdate(mtile.getWorld(),
new Client.Tile(mtile.getFilename())); new Client.Tile(mtile.getFilename()));
} }

View File

@ -1,6 +1,7 @@
package org.dynmap.kzedmap; package org.dynmap.kzedmap;
import org.bukkit.World; import org.bukkit.World;
import org.dynmap.MapChunkCache;
import org.dynmap.Color; import org.dynmap.Color;
import org.dynmap.ConfigurationNode; import org.dynmap.ConfigurationNode;
@ -11,14 +12,15 @@ public class CaveTileRenderer extends DefaultTileRenderer {
} }
@Override @Override
protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result,
MapChunkCache cache) {
boolean air = true; boolean air = true;
result.setTransparent(); result.setTransparent();
for (;;) { for (;;) {
if (y < 0) if (y < 0)
return; return;
int id = world.getBlockTypeIdAt(x, y, z); int id = cache.getBlockTypeID(x, y, z);
if(isnether) { /* Make ceiling into air in nether */ if(isnether) { /* Make ceiling into air in nether */
if(id != 0) if(id != 0)
id = 0; id = 0;

View File

@ -1,7 +1,5 @@
package org.dynmap.kzedmap; package org.dynmap.kzedmap;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster; import java.awt.image.WritableRaster;
import java.io.File; import java.io.File;
@ -18,6 +16,7 @@ import org.dynmap.ColorScheme;
import org.dynmap.ConfigurationNode; import org.dynmap.ConfigurationNode;
import org.dynmap.MapManager; import org.dynmap.MapManager;
import org.dynmap.debug.Debug; import org.dynmap.debug.Debug;
import org.dynmap.MapChunkCache;
public class DefaultTileRenderer implements MapTileRenderer { public class DefaultTileRenderer implements MapTileRenderer {
protected static final Color translucent = new Color(0, 0, 0, 0); protected static final Color translucent = new Color(0, 0, 0, 0);
@ -28,6 +27,7 @@ public class DefaultTileRenderer implements MapTileRenderer {
protected HashSet<Integer> highlightBlocks = new HashSet<Integer>(); protected HashSet<Integer> highlightBlocks = new HashSet<Integer>();
protected Color highlightColor = new Color(255, 0, 0); protected Color highlightColor = new Color(255, 0, 0);
protected int shadowscale[]; /* index=skylight level, value = 256 * scaling value */
@Override @Override
public String getName() { public String getName() {
return name; return name;
@ -41,15 +41,29 @@ public class DefaultTileRenderer implements MapTileRenderer {
if (maximumHeight > 127) if (maximumHeight > 127)
maximumHeight = 127; maximumHeight = 127;
} }
o = configuration.get("shadowstrength");
if(o != null) {
double shadowweight = Double.parseDouble(String.valueOf(o));
if(shadowweight > 0.0) {
shadowscale = new int[16];
for(int i = 0; i < 16; i++) {
double v = 256.0 * (1.0 - (shadowweight * (15-i) / 15.0));
shadowscale[i] = (int)v;
if(shadowscale[i] > 256) shadowscale[i] = 256;
if(shadowscale[i] < 0) shadowscale[i] = 0;
}
}
}
colorScheme = ColorScheme.getScheme((String)configuration.get("colorscheme")); colorScheme = ColorScheme.getScheme((String)configuration.get("colorscheme"));
} }
public boolean render(KzedMapTile tile, File outputFile) { public boolean render(MapChunkCache cache, KzedMapTile tile, File outputFile) {
World world = tile.getWorld(); World world = tile.getWorld();
boolean isnether = (world.getEnvironment() == Environment.NETHER); boolean isnether = (world.getEnvironment() == Environment.NETHER);
BufferedImage im = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); BufferedImage im = KzedMap.allocateBufferedImage(KzedMap.tileWidth, KzedMap.tileHeight);
BufferedImage zim = KzedMap.allocateBufferedImage(KzedMap.tileWidth/2, KzedMap.tileHeight/2);
WritableRaster r = im.getRaster(); WritableRaster r = im.getRaster();
WritableRaster zr = zim.getRaster();
boolean isempty = true; boolean isempty = true;
int ix = KzedMap.anchorx + tile.px / 2 + tile.py / 2 - ((127-maximumHeight)/2); int ix = KzedMap.anchorx + tile.px / 2 + tile.py / 2 - ((127-maximumHeight)/2);
@ -66,30 +80,37 @@ public class DefaultTileRenderer implements MapTileRenderer {
Color c1 = new Color(); Color c1 = new Color();
Color c2 = new Color(); Color c2 = new Color();
int[] rgb = new int[3]; int[] rgb = new int[3*KzedMap.tileWidth];
int[] zrgb = new int[3*KzedMap.tileWidth/2];
/* draw the map */ /* draw the map */
for (y = 0; y < KzedMap.tileHeight;) { for (y = 0; y < KzedMap.tileHeight;) {
jx = ix; jx = ix;
jz = iz; jz = iz;
for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) {
scan(world, jx, iy, jz, 0, isnether, c1); scan(world, jx, iy, jz, 0, isnether, c1, cache);
scan(world, jx, iy, jz, 2, isnether, c2); scan(world, jx, iy, jz, 2, isnether, c2, cache);
if(c1.isTransparent() == false) {
rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue(); rgb[3*x] = c1.getRed();
r.setPixel(x, y, rgb); rgb[3*x+1] = c1.getGreen();
isempty = false; rgb[3*x+2] = c1.getBlue();
} rgb[3*x-3] = c2.getRed();
if(c2.isTransparent() == false) { rgb[3*x-2] = c2.getGreen();
rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue(); rgb[3*x-1] = c2.getBlue();
r.setPixel(x - 1, y, rgb);
isempty = false; isempty = isempty && c1.isTransparent() && c2.isTransparent();
}
jx++; jx++;
jz++; jz++;
} }
r.setPixels(0, y, KzedMap.tileWidth, 1, rgb);
/* Sum up zoomed pixels - bilinar filter */
for(x = 0; x < KzedMap.tileWidth / 2; x++) {
zrgb[3*x] = rgb[6*x] + rgb[6*x+3];
zrgb[3*x+1] = rgb[6*x+1] + rgb[6*x+4];
zrgb[3*x+2] = rgb[6*x+2] + rgb[6*x+5];
}
y++; y++;
@ -97,22 +118,28 @@ public class DefaultTileRenderer implements MapTileRenderer {
jz = iz - 1; jz = iz - 1;
for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) {
scan(world, jx, iy, jz, 2, isnether, c1); scan(world, jx, iy, jz, 2, isnether, c1, cache);
jx++; jx++;
jz++; jz++;
scan(world, jx, iy, jz, 0, isnether, c2); scan(world, jx, iy, jz, 0, isnether, c2, cache);
if(c1.isTransparent() == false) {
rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue();
r.setPixel(x, y, rgb);
isempty = false;
}
if(c2.isTransparent() == false) {
rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue();
r.setPixel(x - 1, y, rgb); rgb[3*x] = c1.getRed();
isempty = false; rgb[3*x+1] = c1.getGreen();
} rgb[3*x+2] = c1.getBlue();
rgb[3*x-3] = c2.getRed();
rgb[3*x-2] = c2.getGreen();
rgb[3*x-1] = c2.getBlue();
isempty = isempty && c1.isTransparent() && c2.isTransparent();
} }
r.setPixels(0, y, KzedMap.tileWidth, 1, rgb);
/* Finish summing values for zoomed pixels */
for(x = 0; x < KzedMap.tileWidth / 2; x++) {
zrgb[3*x] = (zrgb[3*x] + rgb[6*x] + rgb[6*x+3]) >> 2;
zrgb[3*x+1] = (zrgb[3*x+1] + rgb[6*x+1] + rgb[6*x+4]) >> 2;
zrgb[3*x+2] = (zrgb[3*x+2] + rgb[6*x+2] + rgb[6*x+5]) >> 2;
}
zr.setPixels(0, y/2, KzedMap.tileWidth/2, 1, zrgb);
y++; y++;
@ -124,13 +151,14 @@ public class DefaultTileRenderer implements MapTileRenderer {
final File fname = outputFile; final File fname = outputFile;
final KzedMapTile mtile = tile; final KzedMapTile mtile = tile;
final BufferedImage img = im; final BufferedImage img = im;
final BufferedImage zimg = zim;
final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(), final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(),
(KzedMap) mtile.getMap(), mtile); (KzedMap) mtile.getMap(), mtile);
final File zoomFile = MapManager.mapman.getTileFile(zmtile); final File zoomFile = MapManager.mapman.getTileFile(zmtile);
MapManager.mapman.enqueueImageWrite(new Runnable() { MapManager.mapman.enqueueImageWrite(new Runnable() {
public void run() { public void run() {
doFileWrites(fname, mtile, img, zmtile, zoomFile); doFileWrites(fname, mtile, img, zmtile, zoomFile, zimg);
} }
}); });
@ -138,7 +166,8 @@ public class DefaultTileRenderer implements MapTileRenderer {
} }
private void doFileWrites(final File fname, final KzedMapTile mtile, private void doFileWrites(final File fname, final KzedMapTile mtile,
final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile) { final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile,
final BufferedImage zimg) {
Debug.debug("saving image " + fname.getPath()); Debug.debug("saving image " + fname.getPath());
try { try {
ImageIO.write(img, "png", fname); ImageIO.write(img, "png", fname);
@ -147,6 +176,8 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (java.lang.NullPointerException e) { } catch (java.lang.NullPointerException e) {
Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e);
} }
img.flush();
mtile.file = fname; mtile.file = fname;
// Since we've already got the new tile, and we're on an async thread, just // Since we've already got the new tile, and we're on an async thread, just
// make the zoomed tile here // make the zoomed tile here
@ -175,21 +206,20 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (IndexOutOfBoundsException e) { } catch (IndexOutOfBoundsException e) {
} }
boolean zIm_allocated = false;
if (zIm == null) { if (zIm == null) {
/* create new one */ /* create new one */
zIm = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); zIm = KzedMap.allocateBufferedImage(KzedMap.tileWidth, KzedMap.tileHeight);
zIm_allocated = true;
Debug.debug("New zoom-out tile created " + zmtile.getFilename()); Debug.debug("New zoom-out tile created " + zmtile.getFilename());
} else { } else {
Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename()); Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename());
} }
/* blit scaled rendered tile onto zoom-out tile */ /* blit scaled rendered tile onto zoom-out tile */
Graphics2D g2 = zIm.createGraphics(); WritableRaster zim = zIm.getRaster();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); zim.setRect(ox, oy, zimg.getRaster());
g2.drawImage(img, ox, oy, scw, sch, null); KzedMap.freeBufferedImage(zimg);
g2.dispose(); /* Supposed to speed up non-heap memory recovery */
img.flush();
/* save zoom-out tile */ /* save zoom-out tile */
@ -201,7 +231,10 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (java.lang.NullPointerException e) { } catch (java.lang.NullPointerException e) {
Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e);
} }
zIm.flush(); if(zIm_allocated)
KzedMap.freeBufferedImage(zIm);
else
zIm.flush();
/* Push updates for both files.*/ /* Push updates for both files.*/
MapManager.mapman.pushUpdate(mtile.getWorld(), MapManager.mapman.pushUpdate(mtile.getWorld(),
new Client.Tile(mtile.getFilename())); new Client.Tile(mtile.getFilename()));
@ -209,14 +242,15 @@ public class DefaultTileRenderer implements MapTileRenderer {
new Client.Tile(zmtile.getFilename())); new Client.Tile(zmtile.getFilename()));
} }
protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result,
protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { MapChunkCache cache) {
int lightlevel = 15;
result.setTransparent(); result.setTransparent();
for (;;) { for (;;) {
if (y < 0) { if (y < 0) {
return; return;
} }
int id = world.getBlockTypeIdAt(x, y, z); int id = cache.getBlockTypeID(x, y, z);
byte data = 0; byte data = 0;
if(isnether) { /* Make bedrock ceiling into air in nether */ if(isnether) { /* Make bedrock ceiling into air in nether */
if(id != 0) { if(id != 0) {
@ -229,9 +263,29 @@ public class DefaultTileRenderer implements MapTileRenderer {
else else
isnether = false; isnether = false;
} }
if(colorScheme.datacolors[id] != null) { /* If data colored */ if(id != 0) { /* No update needed for air */
data = world.getBlockAt(x, y, z).getData(); if(colorScheme.datacolors[id] != null) { /* If data colored */
data = cache.getBlockData(x, y, z);
}
if((shadowscale != null) && (y < 127)) {
/* Find light level of previous chunk */
switch(seq) {
case 0:
lightlevel = cache.getBlockSkyLight(x, y+1, z);
break;
case 1:
lightlevel = cache.getBlockSkyLight(x+1, y, z);
break;
case 2:
lightlevel = cache.getBlockSkyLight(x, y+1, z);
break;
case 3:
lightlevel = cache.getBlockSkyLight(x, y, z-1);
break;
}
}
} }
switch (seq) { switch (seq) {
case 0: case 0:
x--; x--;
@ -266,16 +320,25 @@ public class DefaultTileRenderer implements MapTileRenderer {
if (c.getAlpha() == 255) { if (c.getAlpha() == 255) {
/* it's opaque - the ray ends here */ /* it's opaque - the ray ends here */
result.setColor(c); result.setColor(c);
if(lightlevel < 15) { /* Not full light? */
shadowColor(result, lightlevel);
}
return; return;
} }
/* this block is transparent, so recurse */ /* this block is transparent, so recurse */
scan(world, x, y, z, seq, isnether, result); scan(world, x, y, z, seq, isnether, result, cache);
int cr = c.getRed(); int cr = c.getRed();
int cg = c.getGreen(); int cg = c.getGreen();
int cb = c.getBlue(); int cb = c.getBlue();
int ca = c.getAlpha(); int ca = c.getAlpha();
if(lightlevel < 15) {
int scale = shadowscale[lightlevel];
cr = (cr * scale) >> 8;
cg = (cg * scale) >> 8;
cb = (cb * scale) >> 8;
}
cr *= ca; cr *= ca;
cg *= ca; cg *= ca;
cb *= ca; cb *= ca;
@ -287,4 +350,10 @@ public class DefaultTileRenderer implements MapTileRenderer {
} }
} }
} }
private final void shadowColor(Color c, int lightlevel) {
int scale = shadowscale[lightlevel];
if(scale < 256)
c.setRGBA((c.getRed() * scale) >> 8, (c.getGreen() * scale) >> 8,
(c.getBlue() * scale) >> 8, c.getAlpha());
}
} }

View File

@ -1,6 +1,7 @@
package org.dynmap.kzedmap; package org.dynmap.kzedmap;
import java.util.HashSet; import java.util.HashSet;
import org.dynmap.MapChunkCache;
import java.util.List; import java.util.List;
import org.bukkit.World; import org.bukkit.World;
@ -19,14 +20,15 @@ public class HighlightTileRenderer extends DefaultTileRenderer {
} }
@Override @Override
protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result,
MapChunkCache cache) {
result.setTransparent(); result.setTransparent();
for (;;) { for (;;) {
if (y < 0) { if (y < 0) {
break; break;
} }
int id = world.getBlockTypeIdAt(x, y, z); int id = cache.getBlockTypeID(x, y, z);
if(isnether) { /* Make bedrock ceiling into air in nether */ if(isnether) { /* Make bedrock ceiling into air in nether */
if(id != 0) { if(id != 0) {
/* Remember first color we see, in case we wind up solid */ /* Remember first color we see, in case we wind up solid */
@ -40,7 +42,7 @@ public class HighlightTileRenderer extends DefaultTileRenderer {
} }
byte data = 0; byte data = 0;
if(colorScheme.datacolors[id] != null) { /* If data colored */ if(colorScheme.datacolors[id] != null) { /* If data colored */
data = world.getBlockAt(x, y, z).getData(); data = cache.getBlockData(x, y, z);
} }
switch (seq) { switch (seq) {

View File

@ -1,7 +1,10 @@
package org.dynmap.kzedmap; package org.dynmap.kzedmap;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -12,6 +15,7 @@ import org.dynmap.DynmapChunk;
import org.dynmap.Log; import org.dynmap.Log;
import org.dynmap.MapTile; import org.dynmap.MapTile;
import org.dynmap.MapType; import org.dynmap.MapType;
import org.dynmap.MapChunkCache;
public class KzedMap extends MapType { public class KzedMap extends MapType {
protected static final Logger log = Logger.getLogger("Minecraft"); protected static final Logger log = Logger.getLogger("Minecraft");
@ -36,6 +40,13 @@ public class KzedMap extends MapType {
MapTileRenderer[] renderers; MapTileRenderer[] renderers;
ZoomedTileRenderer zoomrenderer; ZoomedTileRenderer zoomrenderer;
/* BufferedImage cache - we use the same things a lot... */
private static Object lock = new Object();
private static HashMap<Long, LinkedList<BufferedImage>> imgcache =
new HashMap<Long, LinkedList<BufferedImage>>(); /* Indexed by resolution - X<<32+Y */
private static int[] zerobuf = new int[128];
private static final int CACHE_LIMIT = 10;
public KzedMap(ConfigurationNode configuration) { public KzedMap(ConfigurationNode configuration) {
Log.info("Loading renderers for map '" + getClass().toString() + "'..."); Log.info("Loading renderers for map '" + getClass().toString() + "'...");
List<MapTileRenderer> renderers = configuration.<MapTileRenderer>createInstances("renderers", new Class<?>[0], new Object[0]); List<MapTileRenderer> renderers = configuration.<MapTileRenderer>createInstances("renderers", new Class<?>[0], new Object[0]);
@ -203,12 +214,12 @@ public class KzedMap extends MapType {
} }
@Override @Override
public boolean render(MapTile tile, File outputFile) { public boolean render(MapChunkCache cache, MapTile tile, File outputFile) {
if (tile instanceof KzedZoomedMapTile) { if (tile instanceof KzedZoomedMapTile) {
zoomrenderer.render((KzedZoomedMapTile) tile, outputFile); zoomrenderer.render(cache, (KzedZoomedMapTile) tile, outputFile);
return true; return true;
} else if (tile instanceof KzedMapTile) { } else if (tile instanceof KzedMapTile) {
return ((KzedMapTile) tile).renderer.render((KzedMapTile) tile, outputFile); return ((KzedMapTile) tile).renderer.render(cache, (KzedMapTile) tile, outputFile);
} }
return false; return false;
} }
@ -245,4 +256,48 @@ public class KzedMap extends MapType {
else else
return y - (y % zTileHeight); return y - (y % zTileHeight);
} }
/**
* Allocate buffered image from pool, if possible
* @param x - x dimension
* @param y - y dimension
*/
public static BufferedImage allocateBufferedImage(int x, int y) {
BufferedImage img = null;
synchronized(lock) {
long k = (x<<16) + y;
LinkedList<BufferedImage> ll = imgcache.get(k);
if(ll != null) {
img = ll.poll();
}
}
if(img != null) { /* Got it - reset it for use */
if(zerobuf.length < x)
zerobuf = new int[x];
img.setRGB(0, 0, x, y, zerobuf, 0, 0);
}
else {
img = new BufferedImage(x, y, BufferedImage.TYPE_INT_RGB);
}
return img;
}
/**
* Return buffered image to pool
*/
public static void freeBufferedImage(BufferedImage img) {
img.flush();
synchronized(lock) {
long k = (img.getWidth()<<16) + img.getHeight();
LinkedList<BufferedImage> ll = imgcache.get(k);
if(ll == null) {
ll = new LinkedList<BufferedImage>();
imgcache.put(k, ll);
}
if(ll.size() < CACHE_LIMIT) {
ll.add(img);
img = null;
}
}
}
} }

View File

@ -1,9 +1,10 @@
package org.dynmap.kzedmap; package org.dynmap.kzedmap;
import java.io.File; import java.io.File;
import org.dynmap.MapChunkCache;
public interface MapTileRenderer { public interface MapTileRenderer {
String getName(); String getName();
boolean render(KzedMapTile tile, File outputFile); boolean render(MapChunkCache cache, KzedMapTile tile, File outputFile);
} }

View File

@ -2,12 +2,12 @@ package org.dynmap.kzedmap;
import java.io.File; import java.io.File;
import java.util.Map; import java.util.Map;
import org.dynmap.MapChunkCache;
public class ZoomedTileRenderer { public class ZoomedTileRenderer {
public ZoomedTileRenderer(Map<String, Object> configuration) { public ZoomedTileRenderer(Map<String, Object> configuration) {
} }
public void render(final KzedZoomedMapTile zt, final File outputPath) { public void render(MapChunkCache cache, final KzedZoomedMapTile zt, final File outputPath) {
return; /* Doing this in Default render, since image already loaded */ return; /* Doing this in Default render, since image already loaded */
} }
} }

View File

@ -29,7 +29,7 @@ public class HttpServer extends Thread {
} }
public void startServer() throws IOException { public void startServer() throws IOException {
sock = new ServerSocket(port, 5, bindAddress); sock = new ServerSocket(port, 50, bindAddress); /* 5 too low - more than a couple users during render will get connect errors on some tile loads */
listeningThread = this; listeningThread = this;
start(); start();
Log.info("Dynmap WebServer started on " + bindAddress + ":" + port); Log.info("Dynmap WebServer started on " + bindAddress + ":" + port);