The Last Word in Text Rendering

Ladies and Gentlemen, I am pleased to announce the introduction of a Java 2D based TextRenderer class into the JOGL source tree, in the new package com.sun.opengl.util.j2d. This class provides high performance bitmapped text rendering, with complete Unicode support, into OpenGL drawables with an extremely small and simple API. Advanced string-by-string caching algorithms behind the scenes result in a minimum number of OpenGL pipeline state changes; for a given TextRenderer, all strings’ cached rendering results are placed on a single OpenGL texture. A rectangle packing algorithm manages the placement of the strings on the backing store completely automatically; all details of the caching are hidden from the end user and are not exposed in the public API.

The renderer is extremely easy to use, and looks almost exactly like Java 2D’s Graphics.drawString() API. Here’s a Hello, World code snippet:


  TextRenderer renderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 36));
  renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
  renderer.draw("Hello, World!", 30, 30);
  renderer.endRendering();

There is a simple test of the new code in the jogl-demos workspace in demos.j2d.TestTextRenderer. More demos are coming.

Please take a look and give the new renderer a try and post with any feedback, comments or suggestions. The APIs in this area, including the related TextureRenderer and Overlay classes, are still very much open for consideration, so if you have suggestions about changes or improvements please post them. Also please let us know if you run into any problems, in particular any performance problems, with the new code; it’s been debugged to a certain degree but it’s certainly possible some issues remain, and we’re continuing to look into some situations where performance is not as good as expected.

What would happen if the width of the String exceeds 4096 pixels (or whatever else maximum texture size is in effect)?

How do you deal with state changes? Do you save and restore all changed state (with the obvious performance problems) or do you provide a list of state which may be undefined after using the text renderer?

Ehm… what if you do something like this:


TextRenderer r = new TextRenderer();

int counter = 0;
while(newFrame)
{
   ...
   r.draw("Current frame: "+(counter++));
   ...
}

Wouldn’t that fill the texture after a few frames?

Can’t you have a texture with used characters, and draw the Strings as a series of
quads with proper UVs (selecting the right character). It’s not very likely that the user will
fill the texture with unique characters, as opposed to unique Strings.

If I misunderstood the internal workings of the TextRenderer, please elaborate :slight_smile:

That would be a problem. Internally the renderer would probably fail to allocate its texture. Do you think this is likely to be a real issue? 4096 pixels wide is a pretty long string. We expect that applications are going to handle flowing of text within regions as necessary; this is not handled by this API.

We push and pop a few attributes which are used by the renderer (GL_ENABLE_BIT, GL.GL_DEPTH_BUFFER_BIT, GL.GL_TRANSFORM_BIT) and push and pop the modelview and projection matrices. In theory these are all “downward” calls (no glGet operations) and are intended to be batched up so that multiple strings are rendered in between. In practice I’ve found that the run-time cost of this state pushing/popping is negligible. There are some performance anomalies of some of the underlying code which I’m continuing to look into, but I don’t think the state management is an issue.

No. It’s magic. :slight_smile: If you look at the demos.j2d.TestTextRenderer class, it does exactly this (or something similar – it updates the FPS counter every 100 frames or so), but the backing store remains constant size. The TextRenderer maintains a least recently used cache of strings and reuses the space on the backing store for strings which haven’t been rendered recently, so the older “current frame” strings will be deleted soon.

There are certainly some policy issues to be worked through; your example might currently cause the backing store for the texture to expand to an unreasonably large size, since there isn’t yet a throttle which forces out older strings faster as the pressure on the backing store gets higher. The goal is to make all of these cases work completely automatically and I think it’s achievable.

From my discussions with Phil Race from the Java 2D team, something like this is technically feasible, but also quite complicated. In order to provide complete Unicode support, it isn’t possible to think of the problem in terms of characters (i.e., java "char"s). Instead, you have to think of it in terms of “glyphs”, where a given rendered String will be decomposed into a series of glyphs. In the complete Unicode case, it isn’t reasonable to ask the end user which glyphs to pre-render, so you have to cache them dynamically, and be very careful about composing them on-screen (handling things like bi-directional text, including having left-to-right and right-to-left text in the same String). For all of these reasons, we went down the route of String-by-String caching, and we’ll see how far we get with this approach. If we need to switch to glyph-by-glyph caching, we can do this without changing the public API of the TextRenderer.

Well, there are a bunch of cards with a 512x512 limit (and those voodoo cards with 256x256 max). If the game runs at 800x600 or something higher and you have a screen full of text… oops… big problem.

A simple semi solution would be to use scaling. Determine how big font would be, change the font size until it fits, scale that rect to the desired size. Well, at some point that will also fail and the text gets quickly unreadable etc.

You could split it up into fitting sizes, but you could reach a point where the whole LRU cache gets trashed completely (or even several times per frame).

Or well… dunno. That’s the reason why I asked. Heh. :slight_smile:

Hm.

Well, you could of course clip the thing (screen size enlarged to next max width/height border) before putting it into textures if it exceeds the texture width. Would be fiddly tho… It’s sorta similar to drawing only the visible tiles of a tilemap and you only generate “fresh” tiles if necessary. Makes at least some sense, right? :slight_smile:

For the time being I think we’ll stick with the current caching strategy and see how well it works in peoples’ applications. I’m satisfied that the API doesn’t need to change to support things like glyph-by-glyph caching instead of string-by-string caching, and the current caching strategy seems to be pretty fast at least when you hit in the cache frame to frame. We’ll work through issues as they come in. Please give it a try if you have a chance.

first of all, thanks for this great feature.

i have replaced my current text render (all glyphs prerasterized on a 256x256 texture) with the new jogl TextRenderer and the font is much nicer and crispier but unfortunately i see a framerate drop by 20 fps.

could this be that the cache is running out of size and has to rasterize strings every frame?
my game uses a lot of text. (up to 2000 characters on screen every frame.)

It’s possible. We don’t have a lot of logging code in the TextRenderer right now (we need more) but do you have a before-and-after test case? Could you either file a bug or email me offline (kbr at dev.java.net)?

There was an incredibly stupid bug on my part at the lowest level of the new code underneath the TextRenderer which was causing the backing texture to be deleted and re-created each frame. Thanks to Chris Campbell for helping track this down. The upshot is that the TextRenderer class, as well as the new TextureRenderer and Overlay classes, should now be massively faster, especially as the backing texture gets larger.

emzic, could you please try your new code again with a JOGL nightly build dated 1/6 or later and see whether performance is better with your app?

yes, i will try the new nightly build and report back here as soon as possible. thanks!

ok, with the 1/6 nightly the performance is indeed a lot better. there is almost no framerate difference to my older font renderer.

but there seems to be a problem with the uv coordinates algorithm now. it looks like am getting a “random” rectangle out of the font texture.

i have attached a screenhost if that helps you. thanks!

emzic: is it possible for you to email me a jar file containing your app? Clearly I haven’t done enough debugging of the TextRenderer in real world cases and I can’t guess what’s going wrong just from your screenshot.

Sorry for hijacking, but is there a magic screenshot attached? - coz I can’t see anything… - is this a rights issue with some user(groups) on the board?

http://base.google.com/base_media?q=hand6545221007391880458&size=1

Abusing Google Base as an image host FTW!

Not there for me either.

the error is simply caused by the fact that a texture binding occurs before the rendering.

this simple example shows it. just remove the line with tex.bind(); and it should work.

import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;

import com.sun.opengl.util.Animator;
import com.sun.opengl.util.j2d.TextRenderer;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;

public class TextTest extends Frame implements GLEventListener {

	private static final long serialVersionUID = 1L;

	GLCanvas canvas;

	TextRenderer tr;

	Animator animator;

	Texture tex;

	public TextTest() {
		super("TextTest");
		canvas = new GLCanvas();
		canvas.addGLEventListener(this);
		animator = new Animator(canvas);
		animator.start();
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				new Thread(new Runnable() {
					public void run() {
						animator.stop();
						System.exit(0);
					}
				}).start();
			}
		});
		add(canvas);
		setVisible(true);
		setSize(200, 200);
	}

	public static void main(String[] args) {
		new TextTest();
	}

	public void display(GLAutoDrawable arg0) {
		GL gl = arg0.getGL();
		gl.glClear(GL.GL_COLOR_BUFFER_BIT);
		gl.glLoadIdentity();

		tex.bind(); //comment out this line!
		
		tr.draw("Hello Ken", 10, 100);
	}

	public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) {
	}

	public void init(GLAutoDrawable arg0) {
		GL gl = arg0.getGL();
		gl.glClearColor(0, 0, 0, 1);
		gl.glEnable(GL.GL_TEXTURE_2D);

		try {
			tex = TextureIO.newTexture(new File("font.png"), true);
		} catch (GLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		tr = new TextRenderer(new java.awt.Font("Verdana", java.awt.Font.BOLD, 20), true , false);
	}

	public void reshape(GLAutoDrawable drawable, int x, int y, int width,
			int height) {
		GL gl = drawable.getGL();
		gl.glViewport(0, 0, width, height);
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glOrtho(0, width, 0, height, -1, 1);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
	}
}

Screenshot isn’t visible for anyone in the “Java Core” group.

Kev