Draw text in a background thread or Any efficient text drawing method

Hello,

Drawing text from TTF/OTF font using TextField works fine but is pretty expensive in terms of CPU, especially when having a lot of text on screen.

Is there a way to draw text into a BitmapData object, from a background thread ? More specifically, is it safe to instanciate a TextField object, set its text and format, then draw it into a BitmapData, all in a background thread, or does this HAVE to be done on main Thread?

(I know another option could be to use bitmap fonts, but it’s quite challenging as I need to use quite a lot of different font sizes and colors)

Any help on making text drawing more efficient would be welcome!

(Note that I am still on OpenFL 3 as the project I use depends on Flixel.)

Not sure about text, but as far as I know, everything GPU related have to be done in the main thread (if not, you will probably get segfaults and other hard crashes everywhere like I did, but you can try), which is effectively ENTER_FRAME event. The bitmapData.draw() uses software rendering in 3.x as far as I know, so I’m not sure if it will help either, only if you don’t need to redraw fonts very often.

Thanks for the reply,

Yes, of course. I didn’t mean to make GPU rendering on a separate thread, but mostly drawing text on a bitmap data object using CPU (AFAIK, on OpenFL3, text seems to be rendered on the CPU).

Anyway, for now I did come up with the alternate option I mentioned: using bitmap fonts, downscale/apply tint colors if needed, which solve the issue short term!

Thanks!

If text not changing, you could pre-render textfield to BitmapData.

Yes, that was the initial idea (and I already did that, from the main thread). My question was mostly if this would work from a background thread or not (drawing textfield to bitmapdata), as it would allow to prepare prerendered text in advance without blocking the main loop.

I’ve been using bitmapdata draw in my game on the background thread (c++ and neko only, openfl 3.6) so I could use animated loader while the game was loading. Here is the class I’ve used:

import haxe.Timer;
import openfl.display.BitmapData;
#if cpp
import cpp.vm.Thread;
#elseif neko
import neko.vm.Thread;
#end
import openfl.display.IBitmapDrawable;
import openfl.events.Event;
import openfl.geom.Matrix;
import openfl.Lib;

class EasyThreading
{
	private static var sequence:Array<Dynamic>;
	private static var onSequenceComplete:Dynamic;
	private static var onComplete:Dynamic;
	private static var func:Dynamic;
	
    public static function run(work:Dynamic, onWorkComplete:Void -> Void = null):Void
    {
		func = work;
		onComplete = onWorkComplete;
		Timer.delay(isolate, 10);
    }
	private static function isolate():Void
	{
		Lib.current.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
        #if (cpp || neko)
        var worker:Thread = Thread.create(function()
		{
			var main:Thread = Thread.readMessage(true);
			func();
			main.sendMessage ("Done");
		});
        worker.sendMessage(Thread.current ());
        #else
        func();
        #end
	}
	static private function onEnterFrame(e:Event):Void 
	{
        #if (cpp || neko)
		var message = Thread.readMessage (false);
		
		if (message == "Done") 
		{	
			Lib.current.stage.removeEventListener(Event.ENTER_FRAME, onEnterFrame);
			if (onComplete != null)
				Timer.delay(onComplete, 10);
		}
        #else
        Lib.current.stage.removeEventListener(Event.ENTER_FRAME, onEnterFrame);
        if (onComplete != null)
            Timer.delay(onComplete, 10);
        #end
	}
	public static function draw(bdm:BitmapData, source:IBitmapDrawable, matrix:Matrix, smoothing:Bool, onComplete:Dynamic):Void
	{
		run(function():Void
		{
			bdm.draw(source, matrix, null, null, null, smoothing);
		}, onComplete);
	}
	public static function newSequence(onComplete:Dynamic):Void
	{
		onSequenceComplete = onComplete;
		sequence = [];
	}
	public static function addToSequence(func:Dynamic):Void
	{
		sequence.push(func);
	}
	public static function executeSequence():Void
	{
		if (sequence.length == 0) 
		{
			onSequenceComplete();
			return;
		}
		run(sequence.shift(), executeSequence);
	}
}

And here is an example on how to use it:

class Main extends Sprite 
{
	private var bdm:BitmapData;
	private var textfields:Array<TextField> = [];
	private var circle:Sprite;
	public function new() 
	{
		super();
		
		for (i in 0...5000)
		{
			var tf:TextField = new TextField();
			tf.x = Math.random() * stage.stageWidth;
			tf.y = Math.random() * stage.stageWidth;
			tf.text = "tf " + (i + 1);
			textfields.push(tf);
		}
		bdm = new BitmapData(1024, 1024);
		addChild(new Bitmap(bdm));
		
		circle = new Sprite();
		addChild(circle);
		circle.graphics.beginFill(0xFF0000, 0.5);
		circle.graphics.drawCircle(0, 0, 50);
		circle.x = stage.stageWidth / 2;
		circle.y = stage.stageHeight / 2;
		this.addEventListener(Event.ENTER_FRAME, onEnterFrame);
		EasyThreading.run(drawALot, function()
		{
			removeEventListener(Event.ENTER_FRAME, onEnterFrame);
			trace("done");
		});
	}
	
	private function onEnterFrame(e:Event):Void 
	{
		circle.x += 10;
		if (circle.x > stage.stageWidth + circle.width / 2)
			circle.x = -circle.width / 2;
		trace("main thread");
	}
	
	private function drawALot():Void
	{
		for (i in 0...textfields.length)
		{
			var matrix:Matrix = new Matrix();
			matrix.translate(textfields[i].x, textfields[i].y);
			bdm.draw(textfields[i], matrix);
			trace("background thread");
		}
	}
}

2 Likes

Hey, sorry for late reply, but thanks for you code sample!

While in the related game I ended up using bitmap fonts, this might be handy later, thanks!

Thanks a lot this is a really good way to have another thread make some other things.
Surely reuse!

David.

1 Like