0

Merged in dtt-c-render

Conflicts:
	src/overviewer.h
This commit is contained in:
Andrew Chin
2011-03-20 21:29:05 -04:00
13 changed files with 506 additions and 104 deletions

View File

@@ -246,6 +246,15 @@ Options
--night --night
This option enables --lighting, and renders the world at night. This option enables --lighting, and renders the world at night.
--web-assets-hook=HOOK
This option lets you specify a script to run after the web assets have been
copied into the output directory, but before any tile rendering takes
place. This is an ideal time to do any custom postprocessing for markers.js
or other web assets.
The script should be executable, and it should accept one argument:
the path to the output directory.
Viewing the Results Viewing the Results
------------------- -------------------
Within the output directory you will find two things: an index.html file, and a Within the output directory you will find two things: an index.html file, and a

View File

@@ -46,15 +46,20 @@ image
# alpha_over extension, BUT this extension may fall back to PIL's # alpha_over extension, BUT this extension may fall back to PIL's
# paste(), which DOES need the workaround.) # paste(), which DOES need the workaround.)
def get_lvldata(world,filename, x, y): def get_lvldata(world, filename, x, y, retries=2):
"""Takes a filename and chunkcoords and returns the Level struct, which contains all the """Takes a filename and chunkcoords and returns the Level struct, which contains all the
level info""" level info"""
try: try:
d = world.load_from_region(filename, x, y) d = world.load_from_region(filename, x, y)
except Exception, e: except Exception, e:
logging.warning("Error opening chunk (%i, %i) in %s. It may be corrupt. %s", x, y, filename, e) if retries > 0:
raise ChunkCorrupt(str(e)) # wait a little bit, and try again (up to `retries` times)
time.sleep(1)
return get_lvldata(world, filename, x, y, retries=retries-1)
else:
logging.warning("Error opening chunk (%i, %i) in %s. It may be corrupt. %s", x, y, filename, e)
raise ChunkCorrupt(str(e))
if not d: raise NoSuchChunk(x,y) if not d: raise NoSuchChunk(x,y)
return d[1]['Level'] return d[1]['Level']
@@ -569,7 +574,7 @@ class ChunkRenderer(object):
# check to see if there are any signs in the persistentData list that are from this chunk. # check to see if there are any signs in the persistentData list that are from this chunk.
# if so, remove them from the persistentData list (since they're have been added to the world.POI # if so, remove them from the persistentData list (since they're have been added to the world.POI
# list above # list above.
if self.queue: if self.queue:
self.queue.put(['removePOI', (self.chunkX, self.chunkY)]) self.queue.put(['removePOI', (self.chunkX, self.chunkY)])

View File

@@ -23,12 +23,7 @@ Overviewer. It defaults to the PIL paste function when the custom
alpha-over extension cannot be found. alpha-over extension cannot be found.
""" """
extension_alpha_over = None from c_overviewer import alpha_over as extension_alpha_over
try:
from c_overviewer import alpha_over as _extension_alpha_over
extension_alpha_over = _extension_alpha_over
except ImportError:
pass
def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None): def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None):
"""Composite src over dest, using mask as the alpha channel (if """Composite src over dest, using mask as the alpha channel (if
@@ -40,12 +35,4 @@ def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None):
mask = src mask = src
global extension_alpha_over global extension_alpha_over
if extension_alpha_over is not None: return extension_alpha_over(dest, src, pos_or_rect, mask)
# extension ALWAYS expects rects, so convert if needed
if len(pos_or_rect) == 2:
pos_or_rect = (pos_or_rect[0], pos_or_rect[1], src.size[0], src.size[1])
extension_alpha_over(dest, src, pos_or_rect, mask)
else:
# fallback
dest.paste(src, pos_or_rect, mask)

151
configParser.py Normal file
View File

@@ -0,0 +1,151 @@
from optparse import OptionParser
import sys
import os.path
class OptionsResults(object):
pass
class ConfigOptionParser(object):
def __init__(self, **kwargs):
self.cmdParser = OptionParser(usage=kwargs.get("usage",""))
self.configFile = kwargs.get("config","settings.py")
self.configVars = []
# these are arguments not understood by OptionParser, so they must be removed
# in add_option before being passed to the OptionParser
# note that default is a valid OptionParser argument, but we remove it
# because we want to do our default value handling
self.customArgs = ["required", "commandLineOnly", "default"]
self.requiredArgs = []
def display_config(self):
for x in self.configVars:
n = x['dest']
print "%s: %r" % (n, self.configResults.__dict__[n])
def add_option(self, *args, **kwargs):
if kwargs.get("configFileOnly", False) and kwargs.get("commandLineOnly", False):
raise Exception(args, "configFileOnly and commandLineOnly are mututally exclusive")
self.configVars.append(kwargs.copy())
if not kwargs.get("configFileOnly", False):
for arg in self.customArgs:
if arg in kwargs.keys(): del kwargs[arg]
self.cmdParser.add_option(*args, **kwargs)
def print_help(self):
self.cmdParser.print_help()
def parse_args(self):
# first, load the results from the command line:
options, args = self.cmdParser.parse_args()
# second, use these values to seed the locals dict
l = dict()
g = dict()
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if a.get('commandLineOnly', False): continue
v = getattr(options, n)
if v != None:
#print "seeding %s with %s" % (n, v)
l[n] = v
else:
# if this has a default, use that to seed the globals dict
if a.get("default", None): g[n] = a['default']
g['args'] = args
try:
if os.path.exists(self.configFile):
execfile(self.configFile, g, l)
except NameError, ex:
import traceback
traceback.print_exc()
print "\nError parsing %s. Please check the trackback above" % self.configFile
sys.exit(1)
except SyntaxError, ex:
import traceback
traceback.print_exc()
tb = sys.exc_info()[2]
#print tb.tb_frame.f_code.co_filename
print "\nError parsing %s. Please check the trackback above" % self.configFile
sys.exit(1)
#print l.keys()
configResults = OptionsResults()
# third, load the results from the config file:
for a in self.configVars:
n = a['dest']
if a.get('commandLineOnly', False):
if n in l.keys():
print "Error: %s can only be specified on the command line. It is not valid in the config file" % n
sys.exit(1)
configResults.__dict__[n] = l.get(n)
# third, merge options into configReslts (with options overwriting anything in configResults)
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if getattr(options, n) != None:
configResults.__dict__[n] = getattr(options, n)
# forth, set defaults for any empty values
for a in self.configVars:
n = a['dest']
if (n not in configResults.__dict__.keys() or configResults.__dict__[n] == None) and 'default' in a.keys():
configResults.__dict__[n] = a['default']
# fifth, check required args:
for a in self.configVars:
n = a['dest']
if configResults.__dict__[n] == None and a.get('required',False):
raise Exception("%s is required" % n)
# sixth, check types
for a in self.configVars:
n = a['dest']
if 'type' in a.keys() and configResults.__dict__[n] != None:
try:
# switch on type. there are only 6 types that can be used with optparse
if a['type'] == "int":
configResults.__dict__[n] = int(configResults.__dict__[n])
elif a['type'] == "string":
configResults.__dict__[n] = str(configResults.__dict__[n])
elif a['type'] == "long":
configResults.__dict__[n] = long(configResults.__dict__[n])
elif a['type'] == "choice":
if configResults.__dict__[n] not in a['choices']:
print "The value '%s' is not valid for config parameter '%s'" % (configResults.__dict__[n], n)
sys.exit(1)
elif a['type'] == "float":
configResults.__dict__[n] = long(configResults.__dict__[n])
elif a['type'] == "complex":
configResults.__dict__[n] = complex(configResults.__dict__[n])
elif a['type'] == "function":
if not callable(configResults.__dict__[n]):
raise ValueError("Not callable")
else:
print "Unknown type!"
sys.exit(1)
except ValueError, ex:
print "There was a problem converting the value '%s' to type %s for config parameter '%s'" % (configResults.__dict__[n], a['type'], n)
import traceback
#traceback.print_exc()
sys.exit(1)
self.configResults = configResults
return configResults, args

51
contrib/benchmark.py Normal file
View File

@@ -0,0 +1,51 @@
import chunk
import world
import tempfile
import glob
import time
import cProfile
import os
import sys
import shutil
# Simple Benchmarking script. Usage and example:
# $ python contrib/benchmark.py World4/
# Rendering 50 chunks...
# Took 20.290062 seconds or 0.405801 seconds per chunk, or 2.464261 chunks per second
# create a new, empty, cache dir
cachedir = tempfile.mkdtemp(prefix="benchmark_cache", dir=".")
if os.path.exists("benchmark.prof"): os.unlink("benchmark.prof")
w = world.WorldRenderer("World4", cachedir)
numchunks = 50
chunklist = w._find_chunkfiles()[:numchunks]
print "Rendering %d chunks..." % (numchunks)
def go():
for f in chunklist:
chunk.render_and_save(f[2], w.cachedir, w, (None,None), None)
start = time.time()
if "-profile" in sys.argv:
cProfile.run("go()", 'benchmark.prof')
else:
go()
stop = time.time()
delta = stop - start
print "Took %f seconds or %f seconds per chunk, or %f chunks per second" % (delta, delta/numchunks, numchunks/delta)
if "-profile" in sys.argv:
print "Profile is below:\n----\n"
import pstats
p = pstats.Stats('benchmark.prof')
p.strip_dirs().sort_stats("cumulative").print_stats(20)
shutil.rmtree(cachedir)

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python
import os.path
import sys
overviewer_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]
sys.path.insert(0, overviewer_dir)
import nbt
def check_region(region_filename):
chunk_errors = []
if not os.path.exists(region_filename):
raise Exception('Region file not found: %s' % region_filename)
try:
region = nbt.load_region(region_filename)
except IOError, e:
raise Exception('Error loading region (%s): %s' % (region_filename, e))
try:
chunks = region.get_chunk_info(False)
except IOError, e:
raise Exception('Error reading region header (%s): %s' % (region_filename, e))
except Exception, e:
raise Exception('Error reading region (%s): %s' % (region_filename, e))
for x,y in chunks:
try:
check_chunk(region, x, y)
except Exception, e:
chunk_errors.append(e)
return (chunk_errors, len(chunks))
def check_chunk(region, x, y):
try:
data = region.load_chunk(x ,y)
except Exception, e:
raise Exception('Error reading chunk (%i, %i): %s' % (x, y, e))
if data is None:
raise Exception('Chunk (%i, %i) is unexpectedly empty' % (x, y))
else:
try:
processed_data = data.read_all()
except Exception, e:
raise Exception('Error reading chunk (%i, %i) data: %s' % (x, y, e))
if processed_data == []:
raise Exception('Chunk (%i, %i) is an unexpectedly empty set' % (x, y))
if __name__ == '__main__':
try:
from optparse import OptionParser
parser = OptionParser(usage='python contrib/%prog [OPTIONS] <path/to/regions|path/to/regions/*.mcr|regionfile1.mcr regionfile2.mcr ...>',
description='This script will valide a minecraft region file for errors.')
parser.add_option('-v', dest='verbose', action='store_true', help='Print additional information.')
opts, args = parser.parse_args()
region_files = []
for path in args:
if os.path.isdir(path):
for dirpath, dirnames, filenames in os.walk(path, True):
for filename in filenames:
if filename.startswith('r.') and filename.endswith('.mcr'):
if filename not in region_files:
region_files.append(os.path.join(dirpath, filename))
elif opts.verbose:
print('Ignoring non-region file: %s' % os.path.join(dirpath, filename))
elif os.path.isfile(path):
dirpath,filename = os.path.split(path)
if filename.startswith('r.') and filename.endswith('.mcr'):
if path not in region_files:
region_files.append(path)
else:
print('Ignoring non-region file: %s' % path)
else:
if opts.verbose:
print('Ignoring arg: %s' % path)
if len(region_files) < 1:
print 'You must list at least one region file.'
parser.print_help()
sys.exit(1)
else:
overall_chunk_total = 0
bad_chunk_total = 0
bad_region_total = 0
for region_file in region_files:
try:
(chunk_errors, region_chunks) = check_region(region_file)
bad_chunk_total += len(chunk_errors)
overall_chunk_total += region_chunks
except Exception, e:
bad_region_total += 1
print('FAILED(%s): %s' % (region_file, e))
else:
if len(chunk_errors) is not 0:
print('WARNING(%s) Chunks: %i/%' % (region_file, region_chunks - len(chunk_errors), region_chunks))
if opts.verbose:
for error in chunk_errors:
print(error)
elif opts.verbose:
print ('PASSED(%s) Chunks: %i/%i' % (region_file, region_chunks - len(chunk_errors), region_chunks))
if opts.verbose:
print 'REGIONS: %i/%i' % (len(region_files) - bad_region_total, len(region_files))
print 'CHUNKS: %i/%i' % (overall_chunk_total - bad_chunk_total, overall_chunk_total)
except KeyboardInterrupt:
sys.exit(1)
except Exception, e:
print('ERROR: %s' % e)

42
gmap.py
View File

@@ -22,16 +22,25 @@ if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
import os import os
import os.path import os.path
from optparse import OptionParser from configParser import ConfigOptionParser
import re import re
import subprocess
import multiprocessing import multiprocessing
import time import time
import logging import logging
import optimizeimages
import composite
logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s")
# make sure the c_overviewer extension is available
try:
import c_overviewer
except ImportError:
print "You need to compile the c_overviewer module to run Minecraft Overviewer."
print "Run `python setup.py build`, or see the README for details."
sys.exit(1)
import optimizeimages
import composite
import world import world
import quadtree import quadtree
@@ -44,19 +53,21 @@ def main():
cpus = multiprocessing.cpu_count() cpus = multiprocessing.cpu_count()
except NotImplementedError: except NotImplementedError:
cpus = 1 cpus = 1
parser = OptionParser(usage=helptext)
parser = ConfigOptionParser(usage=helptext, config="settings.py")
parser.add_option("-p", "--processes", dest="procs", help="How many worker processes to start. Default %s" % cpus, default=cpus, action="store", type="int") parser.add_option("-p", "--processes", dest="procs", help="How many worker processes to start. Default %s" % cpus, default=cpus, action="store", type="int")
parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int") parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int", configFileOnly=True)
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true") parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true", commandLineOnly=True)
parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.")
parser.add_option("--lighting", dest="lighting", help="Renders shadows using light data from each chunk.", action="store_true") parser.add_option("--rendermode", dest="rendermode", help="Specifies the render type: normal (default), lighting, night, or spawn.", type="choice", choices=["normal", "lighting", "night", "spawn"], required=True, default="normal")
parser.add_option("--night", dest="night", help="Renders shadows using light data from each chunk, as if it were night. Implies --lighting.", action="store_true") parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.", configFileOnly=True )
parser.add_option("--spawn", dest="spawn", help="Renders shadows using light data from each chunk, as if it were night, while also highlighting areas that are dark enough to spawn mobs. Implies --lighting and --night.", action="store_true") parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%", configFileOnly=True)
parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg.") parser.add_option("--web-assets-hook", dest="web_assets_hook", help="If provided, run this function after the web assets have been copied, but before actual tile rendering begins. It should accept a QuadtreeGen object as its only argument.", action="store", metavar="SCRIPT", type="function", configFileOnly=True)
parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%")
parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.")
parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.")
parser.add_option("--skip-js", dest="skipjs", action="store_true", help="Don't output marker.js or regions.js") parser.add_option("--skip-js", dest="skipjs", action="store_true", help="Don't output marker.js or regions.js")
parser.add_option("--display-config", dest="display_config", action="store_true", help="Display the configuration parameters, but don't render the map. Requires all required options to be specified", commandLineOnly=True)
#parser.add_option("--write-config", dest="write_config", action="store_true", help="Writes out a sample config file", commandLineOnly=True)
options, args = parser.parse_args() options, args = parser.parse_args()
@@ -101,6 +112,11 @@ def main():
parser.error("Where do you want to save the tiles?") parser.error("Where do you want to save the tiles?")
destdir = args[1] destdir = args[1]
if options.display_config:
# just display the config file and exit
parser.display_config()
sys.exit(0)
if options.delete: if options.delete:
return delete_all(worlddir, destdir) return delete_all(worlddir, destdir)
@@ -123,7 +139,7 @@ def main():
optimizeimages.check_programs(optimizeimg) optimizeimages.check_programs(optimizeimg)
else: else:
optimizeimg = None optimizeimg = None
logging.getLogger().setLevel( logging.getLogger().setLevel(
logging.getLogger().level + 10*options.quiet) logging.getLogger().level + 10*options.quiet)
logging.getLogger().setLevel( logging.getLogger().setLevel(
@@ -145,7 +161,7 @@ def main():
# Now generate the tiles # Now generate the tiles
# TODO chunklist # TODO chunklist
q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg, lighting=options.lighting, night=options.night, spawn=options.spawn) q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg, rendermode=options.rendermode, web_assets_hook=options.web_assets_hook)
q.write_html(options.skipjs) q.write_html(options.skipjs)
q.go(options.procs) q.go(options.procs)

View File

@@ -82,8 +82,15 @@ def catch_keyboardinterrupt(func):
raise raise
return newfunc return newfunc
child_quadtree = None
def pool_initializer(quadtree):
logging.debug("Child process {0}".format(os.getpid()))
#stash the quadtree object in a global variable after fork() for windows compat.
global child_quadtree
child_quadtree = quadtree
class QuadtreeGen(object): class QuadtreeGen(object):
def __init__(self, worldobj, destdir, depth=None, tiledir="tiles", imgformat=None, optimizeimg=None, lighting=False, night=False, spawn=False): def __init__(self, worldobj, destdir, depth=None, tiledir="tiles", imgformat=None, optimizeimg=None, rendermode="normal", web_assets_hook=None):
"""Generates a quadtree from the world given into the """Generates a quadtree from the world given into the
given dest directory given dest directory
@@ -96,10 +103,11 @@ class QuadtreeGen(object):
assert(imgformat) assert(imgformat)
self.imgformat = imgformat self.imgformat = imgformat
self.optimizeimg = optimizeimg self.optimizeimg = optimizeimg
self.web_assets_hook = web_assets_hook
self.lighting = lighting self.lighting = rendermode in ("lighting", "night", "spawn")
self.night = night self.night = rendermode in ("night", "spawn")
self.spawn = spawn self.spawn = rendermode in ("spawn",)
# Make the destination dir # Make the destination dir
if not os.path.exists(destdir): if not os.path.exists(destdir):
@@ -189,6 +197,8 @@ class QuadtreeGen(object):
output.write(index) output.write(index)
if skipjs: if skipjs:
if self.web_assets_hook:
self.web_assets_hook(self)
return return
# since we will only discover PointsOfInterest in chunks that need to be # since we will only discover PointsOfInterest in chunks that need to be
@@ -217,6 +227,9 @@ class QuadtreeGen(object):
output.write(' // ]},\n') output.write(' // ]},\n')
output.write('];') output.write('];')
if self.web_assets_hook:
self.web_assets_hook(self)
def _get_cur_depth(self): def _get_cur_depth(self):
"""How deep is the quadtree currently in the destdir? This glances in """How deep is the quadtree currently in the destdir? This glances in
config.js to see what maxZoom is set to. config.js to see what maxZoom is set to.
@@ -292,10 +305,13 @@ class QuadtreeGen(object):
shutil.rmtree(getpath("3")) shutil.rmtree(getpath("3"))
os.rename(getpath("new3"), getpath("3")) os.rename(getpath("new3"), getpath("3"))
def _apply_render_worldtiles(self, pool): def _apply_render_worldtiles(self, pool,batch_size):
"""Returns an iterator over result objects. Each time a new result is """Returns an iterator over result objects. Each time a new result is
requested, a new task is added to the pool and a result returned. requested, a new task is added to the pool and a result returned.
""" """
batch = []
tiles = 0
for path in iterate_base4(self.p): for path in iterate_base4(self.p):
# Get the range for this tile # Get the range for this tile
colstart, rowstart = self._get_range_by_path(path) colstart, rowstart = self._get_range_by_path(path)
@@ -305,30 +321,41 @@ class QuadtreeGen(object):
# This image is rendered at: # This image is rendered at:
dest = os.path.join(self.destdir, self.tiledir, *(str(x) for x in path)) dest = os.path.join(self.destdir, self.tiledir, *(str(x) for x in path))
#logging.debug("this is rendered at %s", dest) #logging.debug("this is rendered at %s", dest)
# Put this in the batch to be submited to the pool
batch.append((colstart, colend, rowstart, rowend, dest))
tiles += 1
if tiles >= batch_size:
tiles = 0
yield pool.apply_async(func=render_worldtile_batch, args= [batch])
batch = []
# And uses these chunks if tiles > 0:
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart, yield pool.apply_async(func=render_worldtile_batch, args= (batch,))
rowend)
#logging.debug(" tilechunks: %r", tilechunks)
# Put this in the pool
# (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate)
yield pool.apply_async(func=render_worldtile, args= (self,
tilechunks, colstart, colend, rowstart, rowend, dest))
def _apply_render_inntertile(self, pool, zoom): def _apply_render_inntertile(self, pool, zoom,batch_size):
"""Same as _apply_render_worltiles but for the inntertile routine. """Same as _apply_render_worltiles but for the inntertile routine.
Returns an iterator that yields result objects from tasks that have Returns an iterator that yields result objects from tasks that have
been applied to the pool. been applied to the pool.
""" """
batch = []
tiles = 0
for path in iterate_base4(zoom): for path in iterate_base4(zoom):
# This image is rendered at: # This image is rendered at:
dest = os.path.join(self.destdir, self.tiledir, *(str(x) for x in path[:-1])) dest = os.path.join(self.destdir, self.tiledir, *(str(x) for x in path[:-1]))
name = str(path[-1]) name = str(path[-1])
yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat, self.optimizeimg)) batch.append((dest, name, self.imgformat, self.optimizeimg))
tiles += 1
if tiles >= batch_size:
tiles = 0
yield pool.apply_async(func=render_innertile_batch, args= [batch])
batch = []
if tiles > 0:
yield pool.apply_async(func=render_innertile_batch, args= [batch])
def go(self, procs): def go(self, procs):
"""Renders all tiles""" """Renders all tiles"""
@@ -344,12 +371,17 @@ class QuadtreeGen(object):
for _ in xrange(curdepth - self.p): for _ in xrange(curdepth - self.p):
self._decrease_depth() self._decrease_depth()
logging.debug("Parent process {0}".format(os.getpid()))
# Create a pool # Create a pool
if procs == 1: if procs == 1:
pool = FakePool() pool = FakePool()
global child_quadtree
child_quadtree = self
else: else:
pool = multiprocessing.Pool(processes=procs) pool = multiprocessing.Pool(processes=procs,initializer=pool_initializer,initargs=(self,))
#warm up the pool so it reports all the worker id's
pool.map(bool,xrange(multiprocessing.cpu_count()),1)
# Render the highest level of tiles from the chunks # Render the highest level of tiles from the chunks
results = collections.deque() results = collections.deque()
complete = 0 complete = 0
@@ -359,20 +391,20 @@ class QuadtreeGen(object):
logging.info("There are {0} total levels to render".format(self.p)) logging.info("There are {0} total levels to render".format(self.p))
logging.info("Don't worry, each level has only 25% as many tiles as the last.") logging.info("Don't worry, each level has only 25% as many tiles as the last.")
logging.info("The others will go faster") logging.info("The others will go faster")
for result in self._apply_render_worldtiles(pool): count = 0
batch_size = 10
for result in self._apply_render_worldtiles(pool,batch_size):
results.append(result) results.append(result)
if len(results) > 10000: if len(results) > (10000/batch_size):
# Empty the queue before adding any more, so that memory # Empty the queue before adding any more, so that memory
# required has an upper bound # required has an upper bound
while len(results) > 500: while len(results) > (500/batch_size):
results.popleft().get() complete += results.popleft().get()
complete += 1
self.print_statusline(complete, total, 1) self.print_statusline(complete, total, 1)
# Wait for the rest of the results # Wait for the rest of the results
while len(results) > 0: while len(results) > 0:
results.popleft().get() complete += results.popleft().get()
complete += 1
self.print_statusline(complete, total, 1) self.print_statusline(complete, total, 1)
self.print_statusline(complete, total, 1, True) self.print_statusline(complete, total, 1, True)
@@ -384,17 +416,15 @@ class QuadtreeGen(object):
complete = 0 complete = 0
total = 4**zoom total = 4**zoom
logging.info("Starting level {0}".format(level)) logging.info("Starting level {0}".format(level))
for result in self._apply_render_inntertile(pool, zoom): for result in self._apply_render_inntertile(pool, zoom,batch_size):
results.append(result) results.append(result)
if len(results) > 10000: if len(results) > (10000/batch_size):
while len(results) > 500: while len(results) > (500/batch_size):
results.popleft().get() complete += results.popleft().get()
complete += 1
self.print_statusline(complete, total, level) self.print_statusline(complete, total, level)
# Empty the queue # Empty the queue
while len(results) > 0: while len(results) > 0:
results.popleft().get() complete += results.popleft().get()
complete += 1
self.print_statusline(complete, total, level) self.print_statusline(complete, total, level)
self.print_statusline(complete, total, level, True) self.print_statusline(complete, total, level, True)
@@ -448,6 +478,14 @@ class QuadtreeGen(object):
return chunklist return chunklist
@catch_keyboardinterrupt @catch_keyboardinterrupt
def render_innertile_batch(batch):
count = 0
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
render_innertile(job[0],job[1],job[2],job[3])
return count
def render_innertile(dest, name, imgformat, optimizeimg): def render_innertile(dest, name, imgformat, optimizeimg):
""" """
Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from
@@ -512,6 +550,30 @@ def render_innertile(dest, name, imgformat, optimizeimg):
optimize_image(imgpath, imgformat, optimizeimg) optimize_image(imgpath, imgformat, optimizeimg)
@catch_keyboardinterrupt @catch_keyboardinterrupt
def render_worldtile_batch(batch):
global child_quadtree
return render_worldtile_batch_(child_quadtree, batch)
def render_worldtile_batch_(quadtree, batch):
count = 0
_get_chunks_in_range = quadtree._get_chunks_in_range
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
colstart = job[0]
colend = job[1]
rowstart = job[2]
rowend = job[3]
path = job[4]
# (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate)
# And uses these chunks
tilechunks = _get_chunks_in_range(colstart, colend, rowstart,rowend)
#logging.debug(" tilechunks: %r", tilechunks)
render_worldtile(quadtree,tilechunks,colstart, colend, rowstart, rowend, path)
return count
def render_worldtile(quadtree, chunks, colstart, colend, rowstart, rowend, path): def render_worldtile(quadtree, chunks, colstart, colend, rowstart, rowend, path):
"""Renders just the specified chunks into a tile and save it. Unlike usual """Renders just the specified chunks into a tile and save it. Unlike usual
python conventions, rowend and colend are inclusive. Additionally, the python conventions, rowend and colend are inclusive. Additionally, the
@@ -529,7 +591,7 @@ def render_worldtile(quadtree, chunks, colstart, colend, rowstart, rowend, path)
Standard tile size has colend-colstart=2 and rowend-rowstart=4 Standard tile size has colend-colstart=2 and rowend-rowstart=4
There is no return value There is no return value
""" """
# width of one chunk is 384. Each column is half a chunk wide. The total # width of one chunk is 384. Each column is half a chunk wide. The total
# width is (384 + 192*(numcols-1)) since the first column contributes full # width is (384 + 192*(numcols-1)) since the first column contributes full
@@ -597,16 +659,10 @@ def render_worldtile(quadtree, chunks, colstart, colend, rowstart, rowend, path)
# check chunk mtimes to see if they are newer # check chunk mtimes to see if they are newer
try: try:
#tile_mtime = os.path.getmtime(imgpath)
regionMtimes = {}
needs_rerender = False needs_rerender = False
for col, row, chunkx, chunky, regionfile in chunks: for col, row, chunkx, chunky, regionfile in chunks:
# check region file mtime first. # check region file mtime first.
# Note: we cache the value since it's actually very likely we will have multipule chunks in the same region, and syscalls are expensive regionMtime = world.get_region_mtime(regionfile)
regionMtime = regionMtimes.get(regionfile,None)
if regionMtime is None:
regionMtime = os.path.getmtime(regionfile)
regionMtimes[regionfile] = regionMtime
if regionMtime <= tile_mtime: if regionMtime <= tile_mtime:
continue continue

View File

@@ -253,6 +253,7 @@ alpha_over_wrap(PyObject *self, PyObject *args)
/* destination position read */ /* destination position read */
if (!PyArg_ParseTuple(pos, "iiii", &dx, &dy, &xsize, &ysize)) { if (!PyArg_ParseTuple(pos, "iiii", &dx, &dy, &xsize, &ysize)) {
/* try again, but this time try to read a point */ /* try again, but this time try to read a point */
PyErr_Clear();
xsize = 0; xsize = 0;
ysize = 0; ysize = 0;
if (!PyArg_ParseTuple(pos, "ii", &dx, &dy)) { if (!PyArg_ParseTuple(pos, "ii", &dx, &dy)) {

View File

@@ -22,6 +22,40 @@
/* macro for getting blockID from a chunk of memory */ /* macro for getting blockID from a chunk of memory */
#define getBlock(blockThing, x,y,z) (*(unsigned char *)(PyArray_GETPTR3(blockThing, (x), (y), (z)))) #define getBlock(blockThing, x,y,z) (*(unsigned char *)(PyArray_GETPTR3(blockThing, (x), (y), (z))))
static PyObject *textures = NULL;
static PyObject *chunk_mod = NULL;
static PyObject *blockmap = NULL;
static PyObject *special_blocks = NULL;
static PyObject *specialblockmap = NULL;
static PyObject *transparent_blocks = NULL;
int init_chunk_render(void) {
/* if blockmap (or any of these) is not NULL, then that means that we've
* somehow called this function twice. error out so we can notice this
* */
if (blockmap) return 1;
textures = PyImport_ImportModule("textures");
chunk_mod = PyImport_ImportModule("chunk");
blockmap = PyObject_GetAttrString(textures, "blockmap");
special_blocks = PyObject_GetAttrString(textures, "special_blocks");
specialblockmap = PyObject_GetAttrString(textures, "specialblockmap");
transparent_blocks = PyObject_GetAttrString(chunk_mod, "transparent_blocks");
/* ensure none of these pointers are NULL */
if ((!transparent_blocks) || (!blockmap) || (!special_blocks) || (!specialblockmap)){
fprintf(stderr, "\ninit_chunk_render failed\n");
return 1;
}
Py_DECREF(textures);
Py_DECREF(chunk_mod);
return 0;
}
static inline int isTransparent(PyObject* tup, unsigned char b) { static inline int isTransparent(PyObject* tup, unsigned char b) {
PyObject *block = PyInt_FromLong(b); PyObject *block = PyInt_FromLong(b);
int ret = PySequence_Contains(tup, block); int ret = PySequence_Contains(tup, block);
@@ -302,8 +336,6 @@ chunk_render(PyObject *self, PyObject *args) {
PyObject *blocks_py; PyObject *blocks_py;
PyObject *textures, *blockmap, *special_blocks, *specialblockmap, *chunk_mod, *transparent_blocks;
int imgx, imgy; int imgx, imgy;
int x, y, z; int x, y, z;
@@ -331,23 +363,6 @@ chunk_render(PyObject *self, PyObject *args) {
PyObject *right_blocks = PyObject_GetAttrString(chunk, "right_blocks"); PyObject *right_blocks = PyObject_GetAttrString(chunk, "right_blocks");
*/ */
textures = PyImport_ImportModule("textures");
chunk_mod = PyImport_ImportModule("chunk");
/* TODO can these be global static? these don't change during program execution */
blockmap = PyObject_GetAttrString(textures, "blockmap");
special_blocks = PyObject_GetAttrString(textures, "special_blocks");
specialblockmap = PyObject_GetAttrString(textures, "specialblockmap");
transparent_blocks = PyObject_GetAttrString(chunk_mod, "transparent_blocks");
if (transparent_blocks == NULL) {
PyErr_SetString(PyExc_ValueError,
"transparent_blocks is NULL");
return NULL;
}
Py_DECREF(textures);
Py_DECREF(chunk_mod);
for (x = 15; x > -1; x--) { for (x = 15; x > -1; x--) {
for (y = 0; y < 16; y++) { for (y = 0; y < 16; y++) {
@@ -427,9 +442,6 @@ chunk_render(PyObject *self, PyObject *args) {
} }
Py_DECREF(blocks_py); Py_DECREF(blocks_py);
Py_DECREF(blockmap);
Py_DECREF(special_blocks);
Py_DECREF(specialblockmap);
return Py_BuildValue("i",2); return Py_BuildValue("i",2);
} }

View File

@@ -35,4 +35,10 @@ initc_overviewer(void)
(void)Py_InitModule("c_overviewer", COverviewerMethods); (void)Py_InitModule("c_overviewer", COverviewerMethods);
/* for numpy */ /* for numpy */
import_array(); import_array();
/* initialize some required variables in iterage.c */
if (init_chunk_render()) {
fprintf(stderr, "failed to init_chunk_render\n");
exit(1); // TODO better way to indicate error?
}
} }

View File

@@ -38,5 +38,6 @@ PyObject *brightness(PyObject *img, double factor);
/* in iterate.c */ /* in iterate.c */
PyObject *chunk_render(PyObject *self, PyObject *args); PyObject *chunk_render(PyObject *self, PyObject *args);
PyObject *chunk_render_lighting(PyObject *self, PyObject *args); PyObject *chunk_render_lighting(PyObject *self, PyObject *args);
int init_chunk_render(void);
#endif /* __OVERVIEWER_H_INCLUDED__ */ #endif /* __OVERVIEWER_H_INCLUDED__ */

View File

@@ -64,7 +64,7 @@ class World(object):
"""Does world-level preprocessing to prepare for QuadtreeGen """Does world-level preprocessing to prepare for QuadtreeGen
worlddir is the path to the minecraft world worlddir is the path to the minecraft world
""" """
mincol = maxcol = minrow = maxrow = 0 mincol = maxcol = minrow = maxrow = 0
def __init__(self, worlddir, useBiomeData=False,regionlist=None, lighting=False): def __init__(self, worlddir, useBiomeData=False,regionlist=None, lighting=False):
@@ -79,7 +79,7 @@ class World(object):
for x, y, regionfile in self._iterate_regionfiles(): for x, y, regionfile in self._iterate_regionfiles():
mcr = nbt.MCRFileReader(regionfile) mcr = nbt.MCRFileReader(regionfile)
mcr.get_chunk_info() mcr.get_chunk_info()
regions[regionfile] = mcr regions[regionfile] = (mcr,os.path.getmtime(regionfile))
regionfiles[(x,y)] = (x,y,regionfile) regionfiles[(x,y)] = (x,y,regionfile)
self.regionfiles = regionfiles self.regionfiles = regionfiles
self.regions = regions self.regions = regions
@@ -118,8 +118,6 @@ class World(object):
_, _, regionfile = self.regionfiles.get((chunkX//32, chunkY//32),(None,None,None)); _, _, regionfile = self.regionfiles.get((chunkX//32, chunkY//32),(None,None,None));
return regionfile return regionfile
def load_from_region(self,filename, x, y): def load_from_region(self,filename, x, y):
nbt = self.load_region(filename).load_chunk(x, y) nbt = self.load_region(filename).load_chunk(x, y)
if nbt is None: if nbt is None:
@@ -128,11 +126,15 @@ class World(object):
return nbt.read_all() return nbt.read_all()
#filo region cache #used to reload a changed region
def load_region(self,filename): def reload_region(self,filename):
#return nbt.MCRFileReader(filename) self.regions[filename] = (nbt.MCRFileReader(filename),os.path.getmtime(regionfile))
return self.regions[filename]
def load_region(self,filename):
return self.regions[filename][0]
def get_region_mtime(self,filename):
return self.regions[filename][1]
def convert_coords(self, chunkx, chunky): def convert_coords(self, chunkx, chunky):
"""Takes a coordinate (chunkx, chunky) where chunkx and chunky are """Takes a coordinate (chunkx, chunky) where chunkx and chunky are