the Fiddler's picture

New text renderer

Project:The Open Toolkit library
Version:0.9.x-dev
Component:Code
Category:task
Priority:normal
Assigned:the Fiddler
Status:closed
Description

I have just committed a WIP implementation of a new text renderer to the text branch. Improvements:

  1. Simpler to use.
  2. More modular. It is now possible to have different GL1/GL2/GL3-level output or text layout providers. It is also possible to specify your own implementations (for example if you wish to integrate this into your engine).
  3. More accurate rendering (better grid fitting).
  4. Improved speed and reduced GC pressure through caching.
  5. Lower texture memory consumption.

The new implementation is based on GdiPMeasureCharacterRanges, which provides better control over text layout, when compared to MeasureString (the old implementation). Text is now cached on two levels, rasterized glyphs (loaded in texture memory) and laid out text. The public API looks like this:

// No TextureFonts, TextHandles or anything.
TextPrinter text = new TextPrinter();
[...]
text.Begin();
text.Print("Hello, World!", font);
text.Print(fps.ToString(), font, TextPrinterOptions.NoCache);
text.End();
[...]
TextExtents extents = text.Measure("ABC", font);
RectangleF bbox = extents.BoundingBox;
RectangleF char_pos = extents[1];  // Returns the position of "B" - useful for e.g. drawing a caret or selecting text.

The implementation in SVN is still under heavy development. Caveats:

  • (Fixed) Single texture sheet limitation remains. This is proving more difficult than expected.
  • (Fixed) Sometimes binds the wrong texture for glyphs (funky results!)
  • Trying to draw / measure uncached strings with newlines will cause GC pressure. This is the unfortunate result of a bug in Mono's GdiPMeasureCharacterRanges implementation. Fortunately, this is unlikely to cause problems in real life (you don't usually create dynamic text with newlines in a tight loop).

Anyone still interested despite these problems, you can test the code from the text branch. Expect screenshots shortly.

Edit: Clarified GC-pressure caveat.


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
JTalton's picture

#41

Let me know when you need someone to test it out.
Grabbed the code and tried to integrate with Golem. It didn't work good but I did not play with it long. I did notice that every time Print is called it calls Begin and sets up the view.
In Golem I had been calling TextPrinter.Begin() -- draw the UI including text - TextPrinter.End(). This was to avoid having to setup the state on every text printing, when the state was barely changing.

the Fiddler's picture

#42

The interface is still in a flux, so I wouldn't advise doing any serious work with it.

Right now, I'm trying to find a good way to handle state batching. Begin() / End() is ok, but it's not nearly reliable enough. It's not exception-safe and it allows you to mess with GL state in all kinds of inventive ways:

TextPrinter printer = new TextPrinter();
[...]
printer.Begin();
GL.Disable(EnableCap.Texture2D);
printer.Print(text, font);
throw new Exception("Good luck recovering now");
printer.End();

One possible way to deal with that is to have some kind of "text batch" object that you can cache, i.e.

TextPrinter printer = new TextPrinter();
[...]
printer.Print(text, font, location); // Equivalent to Begin(); Print(); End();
[...]
TextBatch batch = printer.CreateBatch();
batch.Add(text, font, location);
batch.Add(text2, font2, location2);
[...]
printer.Print(batch); // Equivalent to Begin(); Print(); Print(); End();

The other question is what kind of setup Begin() should do. The increased precision of the new renderer makes it possible to have free-floating text (i.e. not on pixel boundaries) with relatively slight loss of clarity. Should Begin() still set up a resolution-dependent orthographic projection or should it leave the projection matrix intact?

Inertia's picture

#43

XNA's SpriteBatch class does it exactly the same as your last suggestion. You basically build a queue with jobs and SpriteBatch's End() method does all the matrix setup, sorting by shader/texture and drawing operations hidden from the user. This approach is probably best, because all kinds of optimizations can happen in the End() method which do not concern the user.

Regarding the floating text, in which scenario do you see this beneficial compared to rendering the text to a texture attached to FBO? (How about an overload that accepts Math.Matrix4 as parameters?)

JTalton's picture

#44

In my case it gets hard to batch render since the drawing of controls has to happen in a specific order.

At one point I was hoping to have a Widget.Cached property that when set to true, would create a display list and cache all the rendering calls beneath that widget including the other widgets contained in that widget. Since in GL3 display lists are deprecated, I'm not sure what my plan is.

An alternative could be to render to a texture, but that has it's own issues.

I also don't know the absolute positions of the text, only the relative position to it's parent. This is so that I could cache a list of controls, and as a scroll bar/window changes, I could move that whole cached list relative to the scroll. The current text renderer implementation on the branch, calls begin and end in the print function and the begin and end offset the text relative to the current viewport, and I don't have those offsets.

As for what Begin does, I'm not sure it matters, unless it is part of the Print() function. I personally I like it separate so that if I want, I can write my own begin function and manage my own state. Integrating it into the print function ties my hands. Maybe a "bool TextRenderer.AutoBegin" that indicates if to call begin and end in the print function?

JTalton's picture

#45

As part of the interface, would it be possible to add a function to flush all textprinter texture pages? I have thought about letting a user change fonts/scale and it would be nice to flush the old pages and have the printer engine create new pages for just the fonts in use.

the Fiddler's picture

#46

Yes, that is part of the plan.

the Fiddler's picture

#47

Status:in progress (review)» fixed

The code has been committed to trunk.

the Fiddler's picture

#48

Status:fixed» closed

Closing issues fixed in 0.9.2.

the Fiddler's picture

#49

Version:0.9.x-dev»
Mincus's picture

#50

Version:» 0.9.x-dev

I'm struggling to persuade the Linux implementation to draw anywhere other than in the top-left corner (with each new line overwriting the previous).
Following code works under Windows but the location is ignored under Linux (tested under Xubuntu Hardy 32-bit and Xubuntu Intrepid 64-bit).
Seems to happen for both 0.9.3 and 0.9.4, I've not tested earlier.

in OnLoad:

PrintText = new TextPrinter();
TextLocation = new RectangleF(100.0f, 100.0f, 100.0f, 100.0f);
TextToWrite = "OpenTK!";
TextFont = new Font(FontFamily.GenericSansSerif, 16.0f);

in OnRender:

PrintText.Begin();
PrintText.Print(TextToWrite, TextFont, Color.DarkGreen, TextLocation);
PrintText.End();

I'm fairly sure I'm using this correctly, as it works under Windows.
There's plenty of room in the RectangleF and it's at a valid location.

Am I doing something wrong or should I open an issue?
I've attached a test file to demonstrate it if needed.

AttachmentSize
TKText.cs1.52 KB