From 0a1de1b18ec55adf99591defb75c7e79d7a9963f Mon Sep 17 00:00:00 2001 From: Mike Primm Date: Tue, 10 May 2011 11:09:25 +0200 Subject: [PATCH] Add option to run incremental renders on sync thread, using same chunk logic as timesliced full render, in order to handle cases where assumption that all chunks needed for incremental render are already resident (which can be false if player telepots, move quickly, multiple players are active, etc). --- configuration.txt | 3 + .../org/dynmap/DynmapPlayerChatListener.java | 2 +- src/main/java/org/dynmap/MapManager.java | 196 ++++++++++-------- 3 files changed, 117 insertions(+), 84 deletions(-) diff --git a/configuration.txt b/configuration.txt index 5c1e011e..d92888fa 100644 --- a/configuration.txt +++ b/configuration.txt @@ -6,6 +6,9 @@ display-whitelist: false # How often a tile gets rendered (in seconds). renderinterval: 1 +# Do render on main thread - may generate more server load, but safer and fixes broken tiles +renderonsync: true + render-triggers: # - playermove # - playerjoin diff --git a/src/main/java/org/dynmap/DynmapPlayerChatListener.java b/src/main/java/org/dynmap/DynmapPlayerChatListener.java index c732f855..42ada460 100644 --- a/src/main/java/org/dynmap/DynmapPlayerChatListener.java +++ b/src/main/java/org/dynmap/DynmapPlayerChatListener.java @@ -28,4 +28,4 @@ public class DynmapPlayerChatListener extends PlayerListener { plugin.mapManager.pushUpdate(new Client.PlayerQuitMessage(event.getPlayer().getName())); } -} \ No newline at end of file +} diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 30babd7c..a075a366 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -33,17 +33,127 @@ public class MapManager { 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 active_renders = new HashMap(); /* lock for our data structures */ public static final Object lock = new Object(); + private class FullWorldRenderState implements Runnable { + DynmapWorld world; /* Which world are we rendering */ + Location loc; /* Start location */ + int map_index = -1; /* Which map are we on */ + MapType map; + HashSet found = null; + HashSet rendered = null; + LinkedList renderQueue = null; + MapTile tile0 = null; + + /* Full world, all maps render */ + FullWorldRenderState(DynmapWorld dworld, Location l) { + world = dworld; + loc = l; + found = new HashSet(); + rendered = new HashSet(); + renderQueue = new LinkedList(); + } + + /* Single tile render - used for incremental renders */ + FullWorldRenderState(MapTile t) { + world = worlds.get(t.getWorld().getName()); + tile0 = t; + } + + public void run() { + MapTile tile; + + if(tile0 == null) { /* Not single tile render */ + /* If render queue is empty, start next map */ + if(renderQueue.isEmpty()) { + found.clear(); + rendered.clear(); + map_index++; /* Next map */ + if(map_index >= world.maps.size()) { /* Last one done? */ + log.info("Full render finished."); + active_renders.remove(world.world.getName()); + return; + } + map = world.maps.get(map_index); + + /* Now, prime the render queue */ + for (MapTile mt : map.getTiles(loc)) { + if (!found.contains(mt)) { + found.add(mt); + renderQueue.add(mt); + } + } + } + tile = renderQueue.pollFirst(); + } + else { /* Else, single tile render */ + tile = tile0; + } + + DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); + LinkedList loadedChunks = new LinkedList(); + 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 */ + } + else { + 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); + } + /* 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); + } + if(tile0 == null) { /* fullrender */ + /* Schedule the next tile to be worked */ + scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); + } + } + } + public MapManager(DynmapPlugin plugin, ConfigurationNode configuration) { this.tileQueue = new AsynchronousQueue(new Handler() { @Override public void handle(MapTile t) { - render(t); + if(do_sync_render) + scheduler.scheduleSyncDelayedTask(plug_in, + new FullWorldRenderState(t), (int)(timeslice_interval*20)); + else + render(t); } }, (int) (configuration.getDouble("renderinterval", 0.5) * 1000)); @@ -64,6 +174,7 @@ public class MapManager { } do_timesliced_render = configuration.getBoolean("timeslicerender", true); timeslice_interval = configuration.getDouble("timesliceinterval", 0.5); + do_sync_render = configuration.getBoolean("renderonsync", true); scheduler = plugin.getServer().getScheduler(); plug_in = plugin; @@ -71,88 +182,7 @@ public class MapManager { tileQueue.start(); } - private class FullWorldRenderState implements Runnable { - DynmapWorld world; /* Which world are we rendering */ - Location loc; /* Start location */ - int map_index = -1; /* Which map are we on */ - MapType map; - HashSet found = new HashSet(); - HashSet rendered = new HashSet(); - LinkedList renderQueue = new LinkedList(); - - FullWorldRenderState(DynmapWorld dworld, Location l) { - world = dworld; - loc = l; - } - - public void run() { - MapTile tile; - /* If render queue is empty, start next map */ - if(renderQueue.isEmpty()) { - found.clear(); - rendered.clear(); - map_index++; /* Next map */ - if(map_index >= world.maps.size()) { /* Last one done? */ - log.info("Full render finished."); - active_renders.remove(world.world.getName()); - return; - } - map = world.maps.get(map_index); - - /* Now, prime the render queue */ - for (MapTile mt : map.getTiles(loc)) { - if (!found.contains(mt)) { - found.add(mt); - renderQueue.add(mt); - } - } - } - tile = renderQueue.pollFirst(); - - DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); - LinkedList loadedChunks = new LinkedList(); - 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 (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); - /* 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); - } - /* Schedule the next tile to be worked */ - scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); - } - } - + void renderFullWorld(Location l) { DynmapWorld world = worlds.get(l.getWorld().getName());