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
maximumheight: 127
colorscheme: default
# Add shadows to world (based on top-down shadows from chunk data)
# shadowstrength: 1.0
#- class: org.dynmap.kzedmap.HighlightTileRenderer
# prefix: ht
# 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>();
private BukkitScheduler scheduler;
private DynmapPlugin plug_in;
private boolean do_timesliced_render = false;
private double timeslice_interval = 0.0;
private boolean do_sync_render = false; /* Do incremental renders on sync thread too */
/* Which timesliced renders are active */
private HashMap<String, FullWorldRenderState> active_renders = new HashMap<String, FullWorldRenderState>();
@ -96,22 +94,14 @@ public class MapManager {
else { /* Else, single tile render */
tile = tile0;
}
DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile);
LinkedList<DynmapChunk> loadedChunks = new LinkedList<DynmapChunk>();
MapChunkCache cache = new MapChunkCache(world.world, requiredChunks);
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? */
render(tile); /* Just render */
render(cache, tile); /* Just render */
}
else {
if (render(tile)) {
if (render(cache, tile)) {
found.remove(tile);
rendered.add(tile);
for (MapTile adjTile : map.getAdjecentTiles(tile)) {
@ -129,22 +119,7 @@ public class MapManager {
}
}
/* And unload what we loaded */
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);
}
cache.unloadChunks();
if(tile0 == null) { /* fullrender */
/* Schedule the next tile to be worked */
scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20));
@ -159,11 +134,8 @@ public class MapManager {
this.tileQueue = new AsynchronousQueue<MapTile>(new Handler<MapTile>() {
@Override
public void handle(MapTile t) {
if(do_sync_render)
scheduler.scheduleSyncDelayedTask(plug_in,
new FullWorldRenderState(t), 1);
else
render(t);
}
}, (int) (configuration.getDouble("renderinterval", 0.5) * 1000));
@ -175,9 +147,7 @@ public class MapManager {
}
}, 10);
do_timesliced_render = configuration.getBoolean("timeslicerender", true);
timeslice_interval = configuration.getDouble("timesliceinterval", 0.5);
do_sync_render = configuration.getBoolean("renderonsync", true);
for(ConfigurationNode worldConfiguration : configuration.getNodes("worlds")) {
String worldName = worldConfiguration.getString("name");
@ -219,7 +189,6 @@ public class MapManager {
Log.severe("Could not render: world '" + l.getWorld().getName() + "' not defined in configuration.");
return;
}
if(do_timesliced_render) {
String wname = l.getWorld().getName();
FullWorldRenderState rndr = active_renders.get(wname);
if(rndr != null) {
@ -231,66 +200,6 @@ public class MapManager {
/* 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;
}
World w = world.world;
Log.info("Full render starting on world '" + w.getName() + "'...");
for (MapType map : world.maps) {
int requiredChunkCount = 200;
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) {
@ -337,8 +246,8 @@ public class MapManager {
writeQueue.stop();
}
public boolean render(MapTile tile) {
boolean result = tile.getMap().render(tile, getTileFile(tile));
public boolean render(MapChunkCache cache, MapTile tile) {
boolean result = tile.getMap().render(cache, tile, getTileFile(tile));
//Do update after async file write
return result;
@ -387,6 +296,6 @@ public class MapManager {
}
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 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.MapType;
import org.dynmap.debug.Debug;
import org.dynmap.kzedmap.KzedMap;
import org.dynmap.MapChunkCache;
public class FlatMap extends MapType {
private String prefix;
@ -73,13 +75,13 @@ public class FlatMap extends MapType {
}
@Override
public boolean render(MapTile tile, File outputFile) {
public boolean render(MapChunkCache cache, MapTile tile, File outputFile) {
FlatMapTile t = (FlatMapTile) tile;
World w = t.getWorld();
boolean isnether = (w.getEnvironment() == Environment.NETHER) && (maximumHeight == 127);
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();
int[] pixel = new int[4];
@ -93,16 +95,16 @@ public class FlatMap extends MapType {
if(isnether) {
/* Scan until we hit air */
my = 127;
while((blockType = w.getBlockTypeIdAt(mx, my, mz)) != 0) {
while((blockType = cache.getBlockTypeID(mx, my, mz)) != 0) {
my--;
if(my < 0) { /* Solid - use top */
my = 127;
blockType = w.getBlockTypeIdAt(mx, my, mz);
blockType = cache.getBlockTypeID(mx, my, mz);
break;
}
}
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--;
if(my < 0) {
my = 0;
@ -112,14 +114,14 @@ public class FlatMap extends MapType {
}
}
else {
my = w.getHighestBlockYAt(mx, mz) - 1;
my = cache.getHighestBlockYAt(mx, mz) - 1;
if(my > maximumHeight) my = maximumHeight;
blockType = w.getBlockTypeIdAt(mx, my, mz);
blockType = cache.getBlockTypeID(mx, my, mz);
}
byte data = 0;
Color[] colors = colorScheme.colors[blockType];
if(colorScheme.datacolors[blockType] != null) {
data = w.getBlockAt(mx, my, mz).getData();
data = cache.getBlockData(mx, my, mz);
colors = colorScheme.datacolors[blockType][data];
}
if (colors == null)
@ -176,7 +178,7 @@ public class FlatMap extends MapType {
} catch (java.lang.NullPointerException e) {
Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e);
}
img.flush();
KzedMap.freeBufferedImage(img);
MapManager.mapman.pushUpdate(mtile.getWorld(),
new Client.Tile(mtile.getFilename()));
}

View File

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

View File

@ -1,7 +1,5 @@
package org.dynmap.kzedmap;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
@ -18,6 +16,7 @@ import org.dynmap.ColorScheme;
import org.dynmap.ConfigurationNode;
import org.dynmap.MapManager;
import org.dynmap.debug.Debug;
import org.dynmap.MapChunkCache;
public class DefaultTileRenderer implements MapTileRenderer {
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 Color highlightColor = new Color(255, 0, 0);
protected int shadowscale[]; /* index=skylight level, value = 256 * scaling value */
@Override
public String getName() {
return name;
@ -41,15 +41,29 @@ public class DefaultTileRenderer implements MapTileRenderer {
if (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"));
}
public boolean render(KzedMapTile tile, File outputFile) {
public boolean render(MapChunkCache cache, KzedMapTile tile, File outputFile) {
World world = tile.getWorld();
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 zr = zim.getRaster();
boolean isempty = true;
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 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 */
for (y = 0; y < KzedMap.tileHeight;) {
jx = ix;
jz = iz;
for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) {
scan(world, jx, iy, jz, 0, isnether, c1);
scan(world, jx, iy, jz, 2, isnether, c2);
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);
isempty = false;
}
scan(world, jx, iy, jz, 0, isnether, c1, cache);
scan(world, jx, iy, jz, 2, isnether, c2, cache);
rgb[3*x] = c1.getRed();
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();
jx++;
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++;
@ -97,22 +118,28 @@ public class DefaultTileRenderer implements MapTileRenderer {
jz = iz - 1;
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++;
jz++;
scan(world, jx, iy, jz, 0, isnether, c2);
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();
scan(world, jx, iy, jz, 0, isnether, c2, cache);
r.setPixel(x - 1, y, rgb);
isempty = false;
rgb[3*x] = c1.getRed();
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++;
@ -124,13 +151,14 @@ public class DefaultTileRenderer implements MapTileRenderer {
final File fname = outputFile;
final KzedMapTile mtile = tile;
final BufferedImage img = im;
final BufferedImage zimg = zim;
final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(),
(KzedMap) mtile.getMap(), mtile);
final File zoomFile = MapManager.mapman.getTileFile(zmtile);
MapManager.mapman.enqueueImageWrite(new Runnable() {
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,
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());
try {
ImageIO.write(img, "png", fname);
@ -147,6 +176,8 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (java.lang.NullPointerException e) {
Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e);
}
img.flush();
mtile.file = fname;
// Since we've already got the new tile, and we're on an async thread, just
// make the zoomed tile here
@ -175,21 +206,20 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (IndexOutOfBoundsException e) {
}
boolean zIm_allocated = false;
if (zIm == null) {
/* 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());
} else {
Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename());
}
/* blit scaled rendered tile onto zoom-out tile */
Graphics2D g2 = zIm.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(img, ox, oy, scw, sch, null);
g2.dispose(); /* Supposed to speed up non-heap memory recovery */
img.flush();
WritableRaster zim = zIm.getRaster();
zim.setRect(ox, oy, zimg.getRaster());
KzedMap.freeBufferedImage(zimg);
/* save zoom-out tile */
@ -201,6 +231,9 @@ public class DefaultTileRenderer implements MapTileRenderer {
} catch (java.lang.NullPointerException e) {
Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e);
}
if(zIm_allocated)
KzedMap.freeBufferedImage(zIm);
else
zIm.flush();
/* Push updates for both files.*/
MapManager.mapman.pushUpdate(mtile.getWorld(),
@ -209,14 +242,15 @@ public class DefaultTileRenderer implements MapTileRenderer {
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();
for (;;) {
if (y < 0) {
return;
}
int id = world.getBlockTypeIdAt(x, y, z);
int id = cache.getBlockTypeID(x, y, z);
byte data = 0;
if(isnether) { /* Make bedrock ceiling into air in nether */
if(id != 0) {
@ -229,9 +263,29 @@ public class DefaultTileRenderer implements MapTileRenderer {
else
isnether = false;
}
if(id != 0) { /* No update needed for air */
if(colorScheme.datacolors[id] != null) { /* If data colored */
data = world.getBlockAt(x, y, z).getData();
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) {
case 0:
x--;
@ -266,16 +320,25 @@ public class DefaultTileRenderer implements MapTileRenderer {
if (c.getAlpha() == 255) {
/* it's opaque - the ray ends here */
result.setColor(c);
if(lightlevel < 15) { /* Not full light? */
shadowColor(result, lightlevel);
}
return;
}
/* 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 cg = c.getGreen();
int cb = c.getBlue();
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;
cg *= 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;
import java.util.HashSet;
import org.dynmap.MapChunkCache;
import java.util.List;
import org.bukkit.World;
@ -19,14 +20,15 @@ public class HighlightTileRenderer extends DefaultTileRenderer {
}
@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();
for (;;) {
if (y < 0) {
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(id != 0) {
/* Remember first color we see, in case we wind up solid */
@ -40,7 +42,7 @@ public class HighlightTileRenderer extends DefaultTileRenderer {
}
byte data = 0;
if(colorScheme.datacolors[id] != null) { /* If data colored */
data = world.getBlockAt(x, y, z).getData();
data = cache.getBlockData(x, y, z);
}
switch (seq) {

View File

@ -1,7 +1,10 @@
package org.dynmap.kzedmap;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
@ -12,6 +15,7 @@ import org.dynmap.DynmapChunk;
import org.dynmap.Log;
import org.dynmap.MapTile;
import org.dynmap.MapType;
import org.dynmap.MapChunkCache;
public class KzedMap extends MapType {
protected static final Logger log = Logger.getLogger("Minecraft");
@ -36,6 +40,13 @@ public class KzedMap extends MapType {
MapTileRenderer[] renderers;
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) {
Log.info("Loading renderers for map '" + getClass().toString() + "'...");
List<MapTileRenderer> renderers = configuration.<MapTileRenderer>createInstances("renderers", new Class<?>[0], new Object[0]);
@ -203,12 +214,12 @@ public class KzedMap extends MapType {
}
@Override
public boolean render(MapTile tile, File outputFile) {
public boolean render(MapChunkCache cache, MapTile tile, File outputFile) {
if (tile instanceof KzedZoomedMapTile) {
zoomrenderer.render((KzedZoomedMapTile) tile, outputFile);
zoomrenderer.render(cache, (KzedZoomedMapTile) tile, outputFile);
return true;
} else if (tile instanceof KzedMapTile) {
return ((KzedMapTile) tile).renderer.render((KzedMapTile) tile, outputFile);
return ((KzedMapTile) tile).renderer.render(cache, (KzedMapTile) tile, outputFile);
}
return false;
}
@ -245,4 +256,48 @@ public class KzedMap extends MapType {
else
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;
import java.io.File;
import org.dynmap.MapChunkCache;
public interface MapTileRenderer {
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.util.Map;
import org.dynmap.MapChunkCache;
public class ZoomedTileRenderer {
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 */
}
}

View File

@ -29,7 +29,7 @@ public class HttpServer extends Thread {
}
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;
start();
Log.info("Dynmap WebServer started on " + bindAddress + ":" + port);