Skip to content

Commit 72c0271

Browse files
4B796C65@gmail.com4B796C65@gmail.com
authored andcommitted
ctypes related speed optimizations
1 parent 08f4f46 commit 72c0271

File tree

3 files changed

+84
-65
lines changed

3 files changed

+84
-65
lines changed

tdl/__init__.py

Lines changed: 73 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,14 @@
3434
from .__tcod import _lib, _Color, _unpackfile
3535

3636
_IS_PYTHON3 = (sys.version_info[0] == 3)
37-
_USE_FILL = False
38-
'Set to True to use the libtcod fill optimization. This is actually slower than the normal mode.'
3937

40-
def _format_string(string): # still used for filepaths, and that's about it
38+
def _encodeString(string): # still used for filepaths, and that's about it
4139
"changes string into bytes if running in python 3, for sending to ctypes"
4240
if _IS_PYTHON3 and isinstance(string, str):
4341
return string.encode()
4442
return string
4543

46-
#def _encodeString(string):
44+
#def _formatString(string):
4745
# pass
4846

4947
def _formatChar(char):
@@ -93,14 +91,13 @@ def _iscolor(color):
9391
if isinstance(color, int) or not _IS_PYTHON3 and isinstance(color, long):
9492
return True
9593
return False
96-
94+
9795
def _formatColor(color):
9896
"""Format the color to ctypes
9997
"""
10098
if color is None:
10199
return None
102-
# avoid isinstance, checking __class__ gives a small speed increase
103-
if color.__class__ is _Color:
100+
if isinstance(color, _Color):
104101
return color
105102
if isinstance(color, int) or not _IS_PYTHON3 and isinstance(color, long):
106103
# format a web style color with the format 0xRRGGBB
@@ -172,14 +169,23 @@ def drawStr(self, x, y, string, fgcolor=(255, 255, 255), bgcolor=(0, 0, 0)):
172169
assert _verify_colors(fgcolor, bgcolor)
173170
fgcolor, bgcolor = _formatColor(fgcolor), _formatColor(bgcolor)
174171
width, height = self.getSize()
175-
for char in string:
176-
if y == height:
177-
raise TDLError('End of console reached.')
178-
self._setChar(x, y, _formatChar(char), fgcolor, bgcolor)
179-
x += 1 # advance cursor
180-
if x == width: # line break
181-
x = 0
182-
y += 1
172+
batch = [] # prepare a batch operation
173+
def _drawStrGen(x=x, y=y, string=string, width=width, height=height):
174+
"""Generator for drawStr
175+
176+
Iterates over ((x, y), ch) data for _setCharBatch, raising an
177+
error if the end of the console is reached.
178+
"""
179+
for char in string:
180+
if y == height:
181+
raise TDLError('End of console reached.')
182+
#batch.append(((x, y), _formatChar(char))) # ((x, y), ch)
183+
yield((x, y), _formatChar(char))
184+
x += 1 # advance cursor
185+
if x == width: # line break
186+
x = 0
187+
y += 1
188+
self._setCharBatch(_drawStrGen(), fgcolor, bgcolor)
183189

184190
def drawRect(self, x, y, width, height, string, fgcolor=(255, 255, 255), bgcolor=(0, 0, 0)):
185191
"""Draws a rectangle starting from x and y and extending to width and
@@ -198,9 +204,13 @@ def drawRect(self, x, y, width, height, string, fgcolor=(255, 255, 255), bgcolor
198204
assert _verify_colors(fgcolor, bgcolor)
199205
fgcolor, bgcolor = _formatColor(fgcolor), _formatColor(bgcolor)
200206
char = _formatChar(string)
201-
for cellY in range(y, y + height):
202-
for cellX in range(x, x + width):
203-
self._setChar(cellX, cellY, char, fgcolor, bgcolor)
207+
# use itertools to make an x,y grid
208+
# using ctypes here reduces type converstions later
209+
grid = itertools.product((ctypes.c_int(x) for x in range(x, x + width)),
210+
(ctypes.c_int(y) for y in range(y, y + height)))
211+
# zip the single character in a batch variable
212+
batch = zip(grid, itertools.repeat(char, width * height))
213+
self._setCharBatch(batch, fgcolor, bgcolor)
204214

205215
def drawFrame(self, x, y, width, height, string, fgcolor=(255, 255, 255), bgcolor=(0, 0, 0)):
206216
"""Similar to drawRect but only draws the outline of the rectangle.
@@ -442,7 +452,6 @@ def __init__(self, width, height):
442452
self._as_parameter_ = _lib.TCOD_console_new(width, height)
443453
self.width = width
444454
self.height = height
445-
self._initArrays()
446455
#self.clear()
447456

448457
@classmethod
@@ -452,23 +461,9 @@ def _newConsole(cls, console):
452461
self._as_parameter_ = console
453462
self.width = _lib.TCOD_console_get_width(self)
454463
self.height = _lib.TCOD_console_get_height(self)
455-
self._initArrays()
456464
#self.clear()
457465
return self
458466

459-
def _initArrays(self):
460-
if not _USE_FILL:
461-
return
462-
# used for the libtcod fill optimization
463-
IntArray = ctypes.c_int * (self.width * self.height)
464-
self.chArray = IntArray()
465-
self.fgArrays = (IntArray(),
466-
IntArray(),
467-
IntArray())
468-
self.bgArrays = (IntArray(),
469-
IntArray(),
470-
IntArray())
471-
472467
def __del__(self):
473468
"""
474469
If the main console is garbage collected then the window will be closed as well
@@ -517,35 +512,50 @@ def clear(self, fgcolor=(255, 255, 255), bgcolor=(0, 0, 0)):
517512
_lib.TCOD_console_set_default_foreground(self, _formatColor(fgcolor))
518513
_lib.TCOD_console_clear(self)
519514

520-
def _setCharFill(self, x, y, char, fgcolor=None, bgcolor=None):
521-
"""An optimized version using the fill wrappers that didn't work out to be any faster"""
522-
index = x + y * self.width
523-
self.chArray[index] = char
524-
for channel, color in zip(itertools.chain(self.fgArrays, self.bgArrays),
525-
itertools.chain(fgcolor, bgcolor)):
526-
channel[index] = color
527-
528-
def _setCharCall(self, x, y, char, fgcolor=None, bgcolor=None, bgblend=1):
515+
def _setChar(self, x, y, char, fgcolor=None, bgcolor=None, bgblend=1):
529516
"""
530517
Sets a character.
531518
This is called often and is designed to be as fast as possible.
532519
533520
Because of the need for speed this function will do NO TYPE CHECKING
534521
AT ALL, it's up to the drawing functions to use the functions:
535522
_formatChar and _formatColor before passing to this."""
523+
# buffer values as ctypes objects
524+
console = self._as_parameter_
525+
x = ctypes.c_int(x)
526+
y = ctypes.c_int(y)
527+
536528
if char is not None and fgcolor is not None and bgcolor is not None:
537-
return _setcharEX(self, x, y, char, fgcolor, bgcolor)
529+
_setcharEX(console, x, y, char, fgcolor, bgcolor)
530+
return
538531
if char is not None:
539-
_setchar(self, x, y, char)
532+
_setchar(console, x, y, char)
540533
if fgcolor is not None:
541-
_setfore(self, x, y, fgcolor)
534+
_setfore(console, x, y, fgcolor)
542535
if bgcolor is not None:
543-
_setback(self, x, y, bgcolor, bgblend)
536+
_setback(console, x, y, bgcolor, bgblend)
544537

545-
if _USE_FILL:
546-
_setChar = _setCharFill
547-
else:
548-
_setChar = _setCharCall
538+
def _setCharBatch(self, batch, fgcolor, bgcolor, bgblend=1):
539+
"""
540+
Try to perform a batch operation otherwise fall back to _setChar.
541+
If fgcolor and bgcolor are defined then this is faster but not by very
542+
much.
543+
544+
batch is a iterable of [(x, y), ch] items
545+
"""
546+
if fgcolor and bgcolor:
547+
# buffer values as ctypes objects
548+
console = self._as_parameter_
549+
bgblend = ctypes.c_int(bgblend)
550+
551+
_lib.TCOD_console_set_default_background(console, bgcolor)
552+
_lib.TCOD_console_set_default_foreground(console, fgcolor)
553+
_putChar = _lib.TCOD_console_put_char # remove dots
554+
for (x, y), char in batch:
555+
_putChar(console, x, y, char, bgblend)
556+
else:
557+
for (x, y), char in batch:
558+
self._setChar(x, y, char, fgcolor, bgcolor, bgblend)
549559

550560
def getChar(self, x, y):
551561
"""Return the character and colors of a cell as
@@ -556,7 +566,7 @@ def getChar(self, x, y):
556566
557567
@rtype: (int, 3-item tuple, 3-item tuple)
558568
"""
559-
self._drawable(x, y)
569+
assert self._drawable(x, y)
560570
char = _lib.TCOD_console_get_char(self, x, y)
561571
bgcolor = _lib.TCOD_console_get_char_background_wrapper(self, x, y)
562572
fgcolor = _lib.TCOD_console_get_char_foreground_wrapper(self, x, y)
@@ -604,6 +614,12 @@ def clear(self, fgcolor=(255, 255, 255), bgcolor=(0, 0, 0)):
604614
def _setChar(self, x, y, char=None, fgcolor=None, bgcolor=None, bgblend=1):
605615
self.parent._setChar((x + self.x), (y + self.y), char, fgcolor, bgcolor, bgblend)
606616

617+
def _setCharBatch(self, batch, fgcolor, bgcolor, bgblend=1):
618+
myX = self.x # remove dots for speed up
619+
myY = self.y
620+
self.parent._setCharBatch((((x + myX, y + myY), ch) for ((x, y), ch) in batch),
621+
fgcolor, bgcolor, bgblend)
622+
607623
def getChar(self, x, y):
608624
"""Return the character and colors of a cell as (ch, fg, bg)
609625
@@ -667,7 +683,7 @@ def init(width, height, title='python-tdl', fullscreen=False, renderer='OPENGL')
667683
oldroot._replace(rootreplacement)
668684
del rootreplacement
669685

670-
_lib.TCOD_console_init_root(width, height, _format_string(title), fullscreen, renderer)
686+
_lib.TCOD_console_init_root(width, height, _encodeString(title), fullscreen, renderer)
671687

672688
#event.get() # flush the libtcod event queue to fix some issues
673689
# issues may be fixed already
@@ -690,12 +706,6 @@ def flush():
690706
if not _rootinitialized:
691707
raise TDLError('Cannot flush without first initializing with tdl.init')
692708

693-
if _USE_FILL:
694-
console = _rootconsole()
695-
_lib.TCOD_console_fill_background(console, *console.bgArrays)
696-
_lib.TCOD_console_fill_foreground(console, *console.fgArrays)
697-
_lib.TCOD_console_fill_char(console, console.chArray)
698-
699709
_lib.TCOD_console_flush()
700710

701711
def setFont(path, tileWidth, tileHeight, colomn=False,
@@ -757,7 +767,7 @@ def setFont(path, tileWidth, tileHeight, colomn=False,
757767
flags |= FONT_TYPE_GREYSCALE
758768
if not os.path.exists(path):
759769
raise TDLError('no file exists at: "%s"' % path)
760-
_lib.TCOD_console_set_custom_font(_format_string(path), flags, tileWidth, tileHeight)
770+
_lib.TCOD_console_set_custom_font(_encodeString(path), flags, tileWidth, tileHeight)
761771

762772
def getFullscreen():
763773
"""Returns True if program is fullscreen.
@@ -786,7 +796,7 @@ def setTitle(title):
786796
"""
787797
if not _rootinitialized:
788798
raise TDLError('Not initilized. Set title with tdl.init')
789-
_lib.TCOD_console_set_window_title(_format_string(title))
799+
_lib.TCOD_console_set_window_title(_encodeString(title))
790800

791801
def screenshot(path=None):
792802
"""Capture the screen and save it as a png file
@@ -801,10 +811,10 @@ def screenshot(path=None):
801811
if not _rootinitialized:
802812
raise TDLError('Initialize first with tdl.init')
803813
if isinstance(fileobj, str):
804-
_lib.TCOD_sys_save_screenshot(_format_string(fileobj))
814+
_lib.TCOD_sys_save_screenshot(_encodeString(fileobj))
805815
elif isinstance(fileobj, file): # save to temp file and copy to file-like obj
806816
tmpname = os.tempnam()
807-
_lib.TCOD_sys_save_screenshot(_format_string(tmpname))
817+
_lib.TCOD_sys_save_screenshot(_encodeString(tmpname))
808818
with tmpname as tmpfile:
809819
fileobj.write(tmpfile.read())
810820
os.remove(tmpname)
@@ -815,7 +825,7 @@ def screenshot(path=None):
815825
while filename in filelist:
816826
n += 1
817827
filename = 'screenshot%.4i.png' % n
818-
_lib.TCOD_sys_save_screenshot(_format_string(filename))
828+
_lib.TCOD_sys_save_screenshot(_encodeString(filename))
819829
else:
820830
raise TypeError('fileobj is an invalid type: %s' % type(fileobj))
821831

testing/runRegressionTest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def test_drawStrArray(self):
134134
else:
135135
self.console.drawStr(x, y, string, fg, bg)
136136
for ch in string: # inspect console for changes
137-
self.assertEqual(self.console.getChar(x, y), (ch, fg, bg), 'console data should be overwritten')
137+
self.assertEqual(self.console.getChar(x, y), (ch, fg, bg), 'console data should be overwritten, even after an error')
138138
x += 1
139139
if x == width:
140140
x = 0

testing/stressTest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,19 @@ def updateTest(self, deltaTime):
9090
bgcolor = (random.getrandbits(6), random.getrandbits(6), random.getrandbits(6))
9191
self.console.drawRect(0, 0, None, None, ' ', (255, 255, 255), bgcolor)
9292

93+
class DrawStrTest(TestApp):
94+
95+
def updateTest(self, deltaTime):
96+
for y in range(self.height):
97+
bgcolor = (random.getrandbits(6), random.getrandbits(6), random.getrandbits(6))
98+
string = [random.getrandbits(8) for x in range(self.width)]
99+
self.console.drawStr(0, y, string, (255, 255, 255), bgcolor)
100+
93101

94102
def main():
95103
console = tdl.init(60, 40)
96-
for Test in [FullDrawCharTest, CharOnlyTest, ColorOnlyTest, GetCharTest, SingleRectTest]:
104+
for Test in [FullDrawCharTest, CharOnlyTest, ColorOnlyTest, GetCharTest,
105+
SingleRectTest, DrawStrTest]:
97106
Test(console).run()
98107
console.clear()
99108

0 commit comments

Comments
 (0)