Forum Archive

How to improve speed of drawing? Very slow scene view.

marcin_dybas

I made a script using scene module. Basically it shows a different "image" depending on horizontal touch.location.

from scene import *
import ui

glyphsDict = {}  # My dictionary with paths to draw

class MyScene (Scene):
    def setup(self):

        self.myPath = ShapeNode(glyphsDict[1])  # first path to draw
        self.add_child(self.myPath)

        self.background_color = 'lightgrey'

    def touch_began(self, touch):
        x, y = touch.location
        z = int(x/(10.24/2)+1)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

    def touch_moved(self, touch):
        x, y = touch.location
        z = int(x/(10.24/2)+1)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

run(MyScene())

Before that i did the same using SpriteNode and swapping textures. I had 200 .gif images 7KB each (1.4MB summed up) and while the app crashed from time to time i got a rather smooth result if i "loaded" all the images by slowly moving my finger across the screen.

The version with SpriteNode and images looked like this: https://www.instagram.com/p/BSL9ea8g0dv/

I thought i'd move to drawing paths and the code above reflects that. My path information is 200KB so 7 times smaller that all the .gif's i was loading before. But now the animation is super slow and looks like 2fps or is not reacting at all if i move my finger very fast. On the bright side, I don't see the app crashing anymore.

How can i improve on this? Why drawing paths is slower than loading images that are bigger?

JonB

@marcin_dybas it might help if you include the code to generate glyphsDict, so we can actually try this. But off hand, I wonder if it might be faster to construct a list of ShapeNodes in advance, rather than changing the path. I don't really know, just a thought.

How many points do the paths have?

abcabc

Simple shapes seem to work fine as you can see from the following code. As suggested by @JonB you can give us a sample glyphDict that fails.

from scene import *
import ui

glyphsDict = {i:ui.Path.oval(0,0,10+i, 10+i) for i in range(200)}

class MyScene (Scene):
    def setup(self):
        self.center = self.size/2
        self.myPath = ShapeNode(glyphsDict[0], fill_color='red',
                position=self.center,
                parent=self)
        self.background_color = 'grey'

    def touch_began(self, touch):
        x, y = touch.location
        x = x - self.center.x
        z = int(abs(x/5.0))%len(glyphsDict)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

    def touch_moved(self, touch):
        x, y = touch.location
        x = x - self.center.x
        z = int(abs(x/5.0))%len(glyphsDict)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

run(MyScene(), show_fps=True)

abcabc

Even this works.

from scene import *
import ui


from objc_util import *
glyphsDict = {}
for i in range(96):
    font=ObjCClass('UIFont').systemFontOfSize_weight_(32+i,1)
    s='a' #'Hello world'
    p=ui.Path()
    p.line_width = 1
    w=0

    c.CTFontCreatePathForGlyph.restype = c_void_p
    c.CTFontCreatePathForGlyph.argtypes = [c_void_p, c_void_p, c_void_p]

    x = s.encode('ascii')[0]
    glyph=  font._defaultGlyphForChar_(x)
    if not x==32: #space
        letter = ObjCInstance(c.CTFontCreatePathForGlyph(font, glyph, None))
        letterBezier=UIBezierPath.bezierPathWithCGPath_(letter)
        #transform it so we shift and flip y
        letterBezier.applyTransform_(CGAffineTransform(1,0,0,-1,w,font.capHeight()))
        ObjCInstance(p).appendBezierPath_(letterBezier)
    w+=font.advancementForGlyph(glyph).width
    glyphsDict[i] = p

#glyphsDict = {i:ui.Path.oval(0,0,10+i, 10+i) for i in range(200)}

class MyScene (Scene):
    def setup(self):
        self.center = self.size/2
        self.myPath = ShapeNode(glyphsDict[0], stroke_color='red',
                position=self.center,
                parent=self)
        self.background_color = 'grey'

    def touch_began(self, touch):
        x, y = touch.location
        x = x - self.center.x
        z = int(abs(x/5.0))%len(glyphsDict)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

    def touch_moved(self, touch):
        x, y = touch.location
        x = x - self.center.x
        z = int(abs(x/5.0))%len(glyphsDict)
        self.myPath.path = glyphsDict[z]  # Setting a new path to draw

run(MyScene(), show_fps=True)

marcin_dybas

@abcabc All of these work very smooth with 60fps all the time.
@JonB Each path has 29 points.

I uploaded my project on github: https://github.com/dyyybek/pythonista
It's in the "VariableFonts" folder.

The list i'm using to generate the dictionary is in pathsLists.py; It's quite long, it will probably not open in Pythonista.

ccc

I submitted a pull request with some optimizations but I am most interested to know what your iOS device is.

On my 64-bit iPad5,4 your code delivers 60fps unless there is a ton of movement and even then is highly responsive (50+fps).

marcin_dybas

@ccc Thanks for the optimizations. It's an iPad Air. I get under 1fps when moving fast... Is my iPad really that much slower?

JonB

Here is a different approach, using an animated CAShapeLayer
https://gist.github.com/73eb2b7395e41b55e2b4b8b95f303a8b

Neat thing about a shape layer is you can actually have it interpolate for you, for instance you get a smooth animation just tapping the right side then left side of screen.

There is actually a method where you can use just a few paths, and use timeOffset to statically interpolate between them.

abcabc

I am able to reproduce the problem on my ipad ( Pythonista version 3.1.1 (311002) running Python 3.5.1 on iOS 10.1.1 on a 32-bit iPad3,4 with ascreen size of (1024 x 768) * 2) ). It looks like that my ipad is not able to handle large paths and i have done the following modifications to run it on my ipad. I have precomputed the shape nodes and have added a reduction factor. I am able to run with reduction factor 4 and above.
(I have also created a pretty printed version of pathLists file which can be edited on pythonista IDE
https://gist.github.com/balachandrana/de8c3a84ad59be90064108b148a8bd21 )

Other possibility is implementing this in shaders and "2D vector graphics library" in shadertoy could help in this implementation.
https://www.shadertoy.com/view/lslXW8

import scene
from variableTools import glyphsListConstruct

glyphs_list1 = glyphsListConstruct()
reduction_factor = 4.0
x_factor = (10.24 / 2)*reduction_factor


class MyScene(scene.Scene):
    def setup(self):
        self.glyphs_list = [scene.ShapeNode(i) for i in glyphs_list1[::int(reduction_factor)]]
        self.myPath = self.glyphs_list[0]
        self.myPath.anchor_point = 0, 0
        self.myPath.position = (1024 - self.myPath.bbox.width * 1.75,
                                 768 - self.myPath.bbox.height * 1.3)
        self.add_child(self.myPath)
        self.background_color = 'lightgrey'
        self.touch_moved = self.touch_began

    def touch_began(self, touch):
        r = int(touch.location.x / x_factor)%len(self.glyphs_list)
        #print(touch.location.x, r)
        self.myPath.remove_from_parent()
        self.myPath = self.glyphs_list[r]
        self.myPath.anchor_point = 0, 0
        self.myPath.position = (1024 - self.myPath.bbox.width * 1.75,
                                 768 - self.myPath.bbox.height * 1.3)
        self.add_child(self.myPath)


scene.run(MyScene(), show_fps=True)

marcin_dybas

@abcabc This works for me smoothly with reduction factor 2, but with reduction factor 1 the app just crashes.

@JonB This runs very smooth! No lags at all.

i'm starting to understand how to make use of objc_util, thanks to those examples.

I tried above methods with a 1024 paths list. I get a memory error on dictionary creation and the script does not run.
Path information now is only for 1 axis – interpolation between a light and bold font weight. If i want 2 axis interpolation that will quickly go to 10000 for a 100x100 different interpolations... So maybe non of these methods will actually work?

I think my best bet is to make this script with real fonts and utilize the CTFont​Copy​Variation​Axes from CoreText.
(https://developer.apple.com/reference/coretext/ctfont?language=objc#1666125)

I'd be glad if someone would confirm that this is actually possible.

JonB

Since variation axes work by interpolation between a few reference glyphs, you can reproduce that using a paused animation.

See the not very clean
https://gist.github.com/54b80f4fa31365d18ef6e05ed0a98bc9

This works with your original glyphs very nicely, just select the two endpoints. It could be extended where you select which pair to interpolate with, and thats what i started trying.... however the issue seems to be that when you get different weights from CTfont, they are constructed differently... so interpolating ends up jumbling the letter in between. I selected an example where it does work, but it fails for other letters or font weights.

wherever you got your original paths, you just need the 5 or whatever reference glyphs, and then you'd have to modify this a little to adjust toValue and fromValue, along with the relative distance between those values as the layer timeOffset.

marcin_dybas

@JonB said:

however the issue seems to be that when you get different weights from CTfont, they are constructed differently...

If you're trying to do this with some fonts installed on the iPad they probably won't be compatible. The paths need have the same node count and path direction. Here is my sample font with 1 weight axis:

https://www.dropbox.com/s/cbeqxxuk9dg8jz1/PanvariaGX.ttf?dl=0

I included all the code necessary to run the example with your interpolation method:
https://gist.github.com/0e93006203c034a98522601644aff013
This works very well.
I understand that from here it is not very far to use the actual font file?

JonB

https://gist.github.com/8fe4aa269b15a7071633e256456786ca
Here is an improved method, using KeyFrame animations.
Basically, you set up 5 or whatever refernces, add to a PathAnimation, and then provide the interpolation value (0..len(glyphs)).

i have not tried installing your font yet. Might try UIFont.fontWithName_size_traits_(). or maybe need o set up fondDescriptor.

ccc

Can you please confirm if your iOS devices are 32 or 64 bit?

JonB

https://gist.github.com/c8b993ac42ede2574dc9f7cc26ebb1f4
and here is a version that uses your font file, and cleans things up a bit.

I am on 32 bit -- ccc, is this crashing for you? If so, on what line? I think I got typedefs correct but I may have missed a few

ccc

Unfortunately @JonB I confirm that it crashes on 64 bit.

JonB

Which line is crashing? Do you have the fault handler installed?

JonB

https://gist.github.com/7a73b8c3abab4dc502ef2314574c6744

For some reason I was missing a slew of definitions. Try this one on 64bit...

marcin_dybas

@JonB i get "No method found for mutableCopy" on line 80

JonB

https://gist.github.com/80660624600c28c8e7812b15e0652568
How about this one....

marcin_dybas

@JonB this one raises the exception from line 15

JonB

can you paste the full traceback? Also, did you place the Panvaria.ttf file in the same folder? I probably should have mentioned that!

marcin_dybas

@JonB yes, i have the file in the same folder.

Traceback (most recent call last): File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 144, in <module> [glyphs_list, variation_values]=setupGlyphs(size=512,letter='e') File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 78, in setupGlyphs basefont = c.CGFontCreateWithDataProvider(provider); File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 15, in errcheck raise Exception('{} returned void pointer with args\n{}'.format(func.__name__,args)) Exception: CGFontCreateWithDataProvider returned void pointer with args (6242820560,)

JonB

added a little extra diagnostics, and tried passing provider as objcinstancee.
https://gist.github.com/b4c3e011e768320753e3c54405c56380

marcin_dybas

@JonB here's the traceback

Traceback (most recent call last): File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 148, in <module> [glyphs_list, variation_values]=setupGlyphs(size=512,letter='e') File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 82, in setupGlyphs basefont = c.CGFontCreateWithDataProvider(provider); File "/private/var/mobile/Containers/Shared/AppGroup/2CB744A3-0F69-40E1-A5F1-95E464DBFC21/Pythonista3/Documents/PythonistaGit/VF/64bit.py", line 15, in errcheck raise Exception('{} returned void pointer with args\n{}'.format(func.__name__,args)) Exception: CGFontCreateWithDataProvider returned void pointer with args (<b'__NSCFType': <CGDataProvider 0x17418b530>>,)

JonB

How did yoy get the ttf into the device? i think this is saying the font file is invalid.
This version downloads the font directly
https://gist.github.com/7a766eb282c71bf7d27bd13fb4926482

marcin_dybas

@JonB there was indeed something wrong with the file. this script just crashes the app. is there a method to catch errors on crash?

JonB

@dgelessus has a pythonista_startup repo that installs a fault handler and uncaught exception handler which write to a fault log, so you can see which line (for segfaults and the like) or an objc stack trace in the case of objc errors (which often include a helpful message)

https://github.com/dgelessus/pythonista_startup

See the readme, there are several scripts here, personally I just use the enable_faulthandler one.

You could also set a breakpoint by long pressing in the debugger, and just keep single stepping until the crash happens.

JonB

https://gist.github.com/38a5434b4ebd28775dab08d181a00771
Here is a version that logs all cdll calls to glyphlog.txt. if you can post that someplace, we can see exactly what is crashing.

marcin_dybas

@JonB thank you for hints on debugging, definetely helpful.

Here's the error log: https://gist.github.com/5bac67b24317b5e5b4badc06bee1b1c5

JonB

whoops, I missed one of the definitions.
https://gist.github.com/a30ddae506762c96e533f98668ad46dd
This should do it now....maybe

Thought for a future improvement....a CDLL which prevents calling a function unless restype and argtypes have specifically been defined.

marcin_dybas

@JonB unfortunatelly: https://gist.github.com/5f53f356c0c85fa9525efd13a1000198

JonB

well, getting close at least.

try
c.CTFontCreatePathForGlyph.restype = c_void_p
I tried to get fancy, setting restype to a callable, which maybe was a mistake.

Also, I am a little wary of CGGlyph. You could try redefining that. maybe try c_int, or c_int32.

You could also try setting the transform (third argument) to None, just incase there is a problem with the CaTransform.

dgelessus

@JonB Callable restypes are broken and deprecated - if you set restype to a callable, ctypes assumes that the return value is a 32-bit int. Most likely you didn't notice anything because you're on a 32-bit device, but on 64-bit devices this will cut off pointers and things will crash.

I think the currently recommended way to do post-processing on a return value is to set restype to a regular ctypes type, and then set the C function's errcheck attribute as described in the ctypes docs (under CFUNCTYPE probably). This will work regardless of pointer size, because you're telling ctypes the return type via restype.

This is another situation where ctypes makes default choices that silently do the wrong thing on 64-bit machines. If ctypes doesn't know the type of an argument or return value, it defaults to 32-bit ints instead of throwing an error. On 32-bit systems this makes life easier, because almost everything fits into 32 bits, and you almost never need type declarations this way. But on 64-bit systems everything breaks because suddenly half of all ints are 64-bit.

JonB

ok, good to know about callable restype, that was the last one, surely.

A lot of the problems up to this point were due to me forgetting to define restype/argtypes on some parameter, which worked fine for me on 32bit. At some point I think I will subclass CDLL to ensure that argtypes/restype have been user set before allowing call, to prevent this sort of carelessness.

https://gist.github.com/cc991097ddc6b5f28efe22c38105138b

marcin_dybas

@JonB Thank you! This works very well. I won't hesitate to say it would be great to see this wrapped in python so users could open their fonts, read all the axes available and hook up a method to modify each value of the axis then use it with other tools available in Pythonista already. I am now looking up how can i build on top of what you have written. I have some questions still, please look into my comments/questions on your code: https://gist.github.com/dyyybek/810fced40405346682729e8e5568d9a3

Summing up the comments on code:
1. We now have a CTFont object with variations – does it mean we could use it to set type with a layout engine as opposed to drawing paths?
2. Would it be possible to use this font with standard tools in Pythonista?

Is this a place to start looking at how to set a line of text?
Using TextKit

Core Text mediates between text layout and font support provided by higher level frameworks and the low-level capabilities Quartz provides to all text and font frameworks. The Quartz framework acts upon glyphs and their positions. Core Text is aware of how characters map to fonts, and it factors in information about styles, font metrics, and other attributes before it calls into Quartz to render text. Quartz is the only way to get glyphs drawn at a fundamental level, and, because Core Text provides all data in a form directly usable by Quartz, the result is high-performance text rendering.

JonB

I guess we should probably mve this to a git repo.... but oh well,

https://gist.github.com/5f56c0c9e16a98df04b2e23a6198b020

This version uses a ui.TextField -- editable text -- and sliders for size and weight. Then it dynamically gets CTFonts as the sliders move. I was worried this would be too slow to do in realtime, but it seems ok. fonts dont get technically get animated, but the steps seem fine enough that the effect is smooth. I wouldnt be surprised if this code leaks CTfontRefs, but things seem okay after a long time of playing with the sliders, so should be good for casual use.

BTW your PanVaria M and N seem to have issues.