diff options
author | Kenneth Russel <[email protected]> | 2008-02-18 07:58:12 +0000 |
---|---|---|
committer | Kenneth Russel <[email protected]> | 2008-02-18 07:58:12 +0000 |
commit | 91ad31541956e490331b0b111e8676cae5dcfbc5 (patch) | |
tree | 0ffa28310115a352606d28e5255856d51e4c8a7b /src | |
parent | 6f53c43238e16a426e394970aa0fbf377a51b62d (diff) |
Fixed Issue 344: Serious TextRenderer problems involving large fonts & unicode characters
The glyph-based rendering algorithm for the TextRenderer was
performing rendering in two steps: glyph preparation and upload, and
rendering. This structure doesn't work in the context of the
RectanglePacker, which can reorganize the backing store during any
upload.
Restructured the glyph cache in the TextRenderer in terms of flyweight
Glyph objects which know how to upload and render themselves. During
any upload, the outstanding glyphs not yet rendered to the screen may
thereby be flushed. Improved the code path which falls back to the
string-by-string algorithm for complex Unicode characters so that
incoming strings can be segmented into multiple parts which are
rendered either using the glyph cache or the string-by-string
algorithm.
Also tinkered with the bounds of glyphs and strings on the backing
store to try to more definitively eliminate bleed-over between
adjacent characters on the backing store, and to ensure that all of
the pixels of glyphs are drawn. Some heuristics are unfortunately
involved but the new code appears to work well with both very large
and very small fonts.
Added a few more test cases for the TextRenderer based on the bug
report. Tested with the previous test cases as well.
git-svn-id: file:///usr/local/projects/SUN/JOGL/git-svn/svn-server-sync/jogl/trunk@1533 232f8b59-042b-4e1e-8c03-345bb8c30851
Diffstat (limited to 'src')
-rwxr-xr-x | src/classes/com/sun/opengl/util/j2d/TextRenderer.java | 938 |
1 files changed, 462 insertions, 476 deletions
diff --git a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java index edc3475b0..82c60b7d6 100755 --- a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java +++ b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java @@ -124,6 +124,11 @@ import javax.media.opengl.glu.*; */ public class TextRenderer { private static final boolean DEBUG = Debug.debug("TextRenderer"); + + // These are occasionally useful for more in-depth debugging + private static final boolean DISABLE_GLYPH_CACHE = false; + private static final boolean DRAW_BBOXES = false; + static final int kSize = 256; // Every certain number of render cycles, flush the strings which @@ -159,14 +164,6 @@ public class TextRenderer { private Map /*<String,Rect>*/ stringLocations = new HashMap /*<String,Rect>*/(); private GlyphProducer mGlyphProducer; - // Support tokenization of space-separated words - // NOTE: not using this at the present time as we aren't producing - // identical rendering results; may ultimately yield more efficient - // use of the backing store - // private boolean splitAtSpaces = !Debug.isPropertyDefined("jogl.TextRenderer.nosplit"); - private boolean splitAtSpaces = false; - private int spaceWidth = -1; - private java.util.List tokenizationResults = new ArrayList /*<String>*/(); private int numRenderCycles; // Need to keep track of whether we're in a beginRendering() / @@ -301,8 +298,7 @@ public class TextRenderer { this.renderDelegate = renderDelegate; - mGlyphProducer = new GlyphProducer(getFontRenderContext(), - font.getNumGlyphs()); + mGlyphProducer = new GlyphProducer(font.getNumGlyphs()); } /** Returns the bounding rectangle of the given String, assuming it @@ -328,9 +324,7 @@ public class TextRenderer { etc.) the returned bounds correspond to, although every effort is made to ensure an accurate bound. */ public Rectangle2D getBounds(CharSequence str) { - // FIXME: this doesn't hit the cache if tokenization is enabled -- - // needs more work - // Prefer a more optimized approach + // FIXME: this should be more optimized and use the glyph cache Rect r = null; if ((r = (Rect) stringLocations.get(str)) != null) { @@ -547,25 +541,6 @@ public class TextRenderer { endRendering(true); } - /** Returns the width of the ASCII space character, in pixels, drawn - in this TextRenderer's font when no scaling or rotation has been - applied. This is the horizontal advance of the space character. - - @return the width of the space character in the TextRenderer's font - */ - private int getSpaceWidth() { - if (spaceWidth < 0) { - Graphics2D g = getGraphics2D(); - - FontRenderContext frc = getFontRenderContext(); - GlyphVector gv = font.createGlyphVector(frc, " "); - GlyphMetrics metrics = gv.getGlyphMetrics(0); - spaceWidth = (int) metrics.getAdvanceX(); - } - - return spaceWidth; - } - /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}. Restores several OpenGL state bits. Should be paired with {@link #begin3DRendering begin3DRendering}. @@ -596,14 +571,34 @@ public class TextRenderer { //---------------------------------------------------------------------- // Internals only below this point // - private static Rectangle2D normalize(Rectangle2D src) { - // Give ourselves a one-pixel boundary around each string in order - // to prevent bleeding of nearby Strings due to the fact that we - // use linear filtering - return new Rectangle2D.Double((int) Math.floor(src.getMinX() - 1), - (int) Math.floor(src.getMinY() - 1), - (int) Math.ceil(src.getWidth() + 2), - (int) Math.ceil(src.getHeight()) + 2); + + private static Rectangle2D preNormalize(Rectangle2D src) { + // Need to round to integer coordinates + // Also give ourselves a little slop around the reported + // bounds of glyphs because it looks like neither the visual + // nor the pixel bounds works perfectly well + int minX = (int) Math.floor(src.getMinX()) - 1; + int minY = (int) Math.floor(src.getMinY()) - 1; + int maxX = (int) Math.ceil(src.getMaxX()) + 1; + int maxY = (int) Math.ceil(src.getMaxY()) + 1; + return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + } + + + private Rectangle2D normalize(Rectangle2D src) { + // Give ourselves a boundary around each entity on the backing + // store in order to prevent bleeding of nearby Strings due to + // the fact that we use linear filtering + + // NOTE that this boundary is quite heuristic and is related + // to how far away in 3D we may view the text -- + // heuristically, 1.5% of the font's height + int boundary = (int) Math.max(1, 0.015 * font.getSize()); + + return new Rectangle2D.Double((int) Math.floor(src.getMinX() - boundary), + (int) Math.floor(src.getMinY() - boundary), + (int) Math.ceil(src.getWidth() + 2 * boundary), + (int) Math.ceil(src.getHeight()) + 2 * boundary); } private TextureRenderer getBackingStore() { @@ -744,44 +739,6 @@ public class TextRenderer { } } - private void tokenize(CharSequence str) { - // Avoid lots of little allocations per render - tokenizationResults.clear(); - - if (!splitAtSpaces) { - tokenizationResults.add(str.toString()); - } else { - int startChar = 0; - char c = (char) 0; - int len = str.length(); - int i = 0; - - while (i < len) { - if (str.charAt(i) == ' ') { - // Terminate any substring - if (startChar < i) { - tokenizationResults.add(str.subSequence(startChar, i) - .toString()); - } else { - tokenizationResults.add(null); - } - - startChar = i + 1; - } - - ++i; - } - - // Add on any remaining (all?) characters - if (startChar == 0) { - tokenizationResults.add(str); - } else if (startChar < len) { - tokenizationResults.add(str.subSequence(startChar, len) - .toString()); - } - } - } - private void clearUnusedEntries() { final java.util.List deadRects = new ArrayList /*<Rect>*/(); @@ -839,19 +796,11 @@ public class TextRenderer { private void internal_draw3D(CharSequence str, float x, float y, float z, float scaleFactor) { - int drawingState = DrawingState.fast; - - while (drawingState != DrawingState.finished) { - GlyphsList glyphs = mGlyphProducer.getGlyphs(str); - - if (drawingState == DrawingState.fast) { - x += drawGlyphs(glyphs, x, y, z, scaleFactor); - str = glyphs.remaining; - drawingState = glyphs.nextState; - } else if (drawingState == DrawingState.robust) { - this.draw3D_ROBUST(str, x, y, z, scaleFactor); - drawingState = DrawingState.finished; - } + List/*<Glyph>*/ glyphs = mGlyphProducer.getGlyphs(str); + for (Iterator iter = glyphs.iterator(); iter.hasNext(); ) { + Glyph glyph = (Glyph) iter.next(); + float advance = glyph.draw3D(x, y, z, scaleFactor); + x += advance * scaleFactor; } } @@ -861,161 +810,85 @@ public class TextRenderer { } } - private float drawGlyphs(GlyphsList inGlyphs, float inX, float inY, - float z, float scaleFactor) { - float xOffset = 0; - - try { - if (mPipelinedQuadRenderer == null) { - mPipelinedQuadRenderer = new Pipelined_QuadRenderer(); - } - - TextureRenderer renderer = getBackingStore(); - // Handles case where NPOT texture is used for backing store - TextureCoords wholeImageTexCoords = renderer.getTexture().getImageTexCoords(); - float xScale = wholeImageTexCoords.right(); - float yScale = wholeImageTexCoords.bottom(); - - for (int i = 0; i < inGlyphs.length; i++) { - Rect rect = inGlyphs.textureSourceRect[i]; - TextData data = (TextData) rect.getUserData(); - data.markUsed(); - - float x = (inX + xOffset) - (scaleFactor * data.origin().x); - float y = inY - (scaleFactor * (rect.h() - data.origin().y)); - - int texturex = rect.x(); // avoid overpump of textureUpload path by not triggering sync every quad, instead doing it on flushGlyphPipeline - int texturey = renderer.getHeight() - rect.y() - rect.h(); - int width = rect.w(); - int height = rect.h(); - - float tx1 = xScale * (float) texturex / (float) renderer.getWidth(); - float ty1 = yScale * (1.0f - - ((float) texturey / (float) renderer.getHeight())); - float tx2 = xScale * (float) (texturex + width) / (float) renderer.getWidth(); - float ty2 = yScale * (1.0f - - ((float) (texturey + height) / (float) renderer.getHeight())); - - mPipelinedQuadRenderer.glTexCoord2f(tx1, ty1); - mPipelinedQuadRenderer.glVertex3f(x, y, z); - mPipelinedQuadRenderer.glTexCoord2f(tx2, ty1); - mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), y, - z); - mPipelinedQuadRenderer.glTexCoord2f(tx2, ty2); - mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), - y + (height * scaleFactor), z); - mPipelinedQuadRenderer.glTexCoord2f(tx1, ty2); - mPipelinedQuadRenderer.glVertex3f(x, - y + (height * scaleFactor), z); - - xOffset += (inGlyphs.advances[i] * scaleFactor); // note the advances.. I had to use this to get proper kerning. - } - } catch (Exception e) { - e.printStackTrace(); - } - - return xOffset; - } - - private float drawGlyphsSIMPLE(GlyphsList inGlyphs, float x, float y, - float z, float scaleFactor) // unused, for reference, debugging - { - TextureRenderer renderer = getBackingStore(); - - int xOffset = 0; - - for (int i = 0; i < inGlyphs.length; i++) { - Rect rect = inGlyphs.textureSourceRect[i]; - - if (rect != null) { - TextData data = (TextData) rect.getUserData(); - data.markUsed(); - - renderer.draw3DRect((x + xOffset) - - (scaleFactor * data.origin().x), // forces upload every new glyph - y - (scaleFactor * (rect.h() - data.origin().y)), z, - rect.x(), renderer.getHeight() - rect.y() - rect.h(), - rect.w(), rect.h(), scaleFactor); - - xOffset += (int) (((inGlyphs.advances[i]) * scaleFactor) + - 0.5f); // note the advances.. I had to use this to get proper kerning. - } - } - - return xOffset; - } - private void draw3D_ROBUST(CharSequence str, float x, float y, float z, float scaleFactor) { - // Split up the string into space-separated pieces - tokenize(str); - - int xOffset = 0; - - for (Iterator iter = tokenizationResults.iterator(); iter.hasNext();) { - String curStr = (String) iter.next(); // no tokenization needed, because it was done to shrink # of uniques - - if (curStr != null) { - // Look up the string on the backing store - Rect rect = (Rect) stringLocations.get(curStr); - - if (rect == null) { - // Rasterize this string and place it on the backing store - Graphics2D g = getGraphics2D(); - Rectangle2D bbox = normalize(renderDelegate.getBounds(curStr, font, getFontRenderContext())); - Point origin = new Point((int) -bbox.getMinX(), - (int) -bbox.getMinY()); - rect = new Rect(0, 0, (int) bbox.getWidth(), - (int) bbox.getHeight(), - new TextData(curStr, origin, -1)); - - packer.add(rect); - stringLocations.put(curStr, rect); - - // Re-fetch the Graphics2D in case the addition of the rectangle - // caused the old backing store to be thrown away - g = getGraphics2D(); - - // OK, should now have an (x, y) for this rectangle; rasterize - // the String - // FIXME: need to verify that this causes the String to be - // rasterized fully into the bounding rectangle - int strx = rect.x() + origin.x; - int stry = rect.y() + origin.y; - - // Clear out the area we're going to draw into - g.setComposite(AlphaComposite.Clear); - g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); - g.setComposite(AlphaComposite.Src); - - // Draw the string - renderDelegate.draw(g, curStr, strx, stry); - - // Mark this region of the TextureRenderer as dirty - getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), - rect.h()); - } + String curStr; + if (str instanceof String) { + curStr = (String) str; + } else { + curStr = str.toString(); + } - // OK, now draw the portion of the backing store to the screen - TextureRenderer renderer = getBackingStore(); + // Look up the string on the backing store + Rect rect = (Rect) stringLocations.get(curStr); - // NOTE that the rectangles managed by the packer have their - // origin at the upper-left but the TextureRenderer's origin is - // at its lower left!!! + if (rect == null) { + // Rasterize this string and place it on the backing store + Graphics2D g = getGraphics2D(); + Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(curStr, font, getFontRenderContext())); + Rectangle2D bbox = normalize(origBBox); + Point origin = new Point((int) -bbox.getMinX(), + (int) -bbox.getMinY()); + rect = new Rect(0, 0, (int) bbox.getWidth(), + (int) bbox.getHeight(), + new TextData(curStr, origin, origBBox, -1)); + + packer.add(rect); + stringLocations.put(curStr, rect); + + // Re-fetch the Graphics2D in case the addition of the rectangle + // caused the old backing store to be thrown away + g = getGraphics2D(); + + // OK, should now have an (x, y) for this rectangle; rasterize + // the String + int strx = rect.x() + origin.x; + int stry = rect.y() + origin.y; + + // Clear out the area we're going to draw into + g.setComposite(AlphaComposite.Clear); + g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); + g.setComposite(AlphaComposite.Src); + + // Draw the string + renderDelegate.draw(g, curStr, strx, stry); + + if (DRAW_BBOXES) { TextData data = (TextData) rect.getUserData(); - data.markUsed(); - - // Align the leftmost point of the baseline to the (x, y, z) coordinate requested - renderer.draw3DRect((x + xOffset) - - (scaleFactor * data.origin().x), - y - (scaleFactor * (rect.h() - data.origin().y)), z, - rect.x(), renderer.getHeight() - rect.y() - rect.h(), - rect.w(), rect.h(), scaleFactor); - xOffset += (rect.w() * scaleFactor); + // Draw a bounding box on the backing store + g.drawRect(strx - data.origOriginX(), + stry - data.origOriginY(), + (int) data.origRect().getWidth(), + (int) data.origRect().getHeight()); + g.drawRect(strx - data.origin().x, + stry - data.origin().y, + rect.w(), + rect.h()); } - xOffset += (getSpaceWidth() * scaleFactor); + // Mark this region of the TextureRenderer as dirty + getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), + rect.h()); } + + // OK, now draw the portion of the backing store to the screen + TextureRenderer renderer = getBackingStore(); + + // NOTE that the rectangles managed by the packer have their + // origin at the upper-left but the TextureRenderer's origin is + // at its lower left!!! + TextData data = (TextData) rect.getUserData(); + data.markUsed(); + + Rectangle2D origRect = data.origRect(); + + // Align the leftmost point of the baseline to the (x, y, z) coordinate requested + renderer.draw3DRect(x - (scaleFactor * data.origOriginX()), + y - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())), z, + rect.x() + (data.origin().x - data.origOriginX()), + renderer.getHeight() - rect.y() - (int) origRect.getHeight() - + (data.origin().y - data.origOriginY()), + (int) origRect.getWidth(), (int) origRect.getHeight(), scaleFactor); } //---------------------------------------------------------------------- @@ -1108,16 +981,15 @@ public class TextRenderer { int x, int y); } - private static class MapCharSequenceToGlyphVector - implements CharacterIterator { + private static class CharSequenceIterator implements CharacterIterator { CharSequence mSequence; int mLength; int mCurrentIndex; - MapCharSequenceToGlyphVector() { + CharSequenceIterator() { } - MapCharSequenceToGlyphVector(CharSequence sequence) { + CharSequenceIterator(CharSequence sequence) { initFromCharSequence(sequence); } @@ -1172,7 +1044,7 @@ public class TextRenderer { } public Object clone() { - MapCharSequenceToGlyphVector iter = new MapCharSequenceToGlyphVector(mSequence); + CharSequenceIterator iter = new CharSequenceIterator(mSequence); iter.mCurrentIndex = mCurrentIndex; return iter; @@ -1191,8 +1063,13 @@ public class TextRenderer { // Data associated with each rectangle of text static class TextData { + // Back-pointer to String this TextData describes, if it + // represents a String rather than a single glyph + private String str; + + // If this TextData represents a single glyph, this is its + // unicode ID int unicodeID; - private String str; // Back-pointer to String this TextData describes // The following must be defined and used VERY precisely. This is // the offset from the upper-left corner of this rectangle (Java @@ -1200,11 +1077,21 @@ public class TextRenderer { // order to fit within the rectangle -- the leftmost point of the // baseline. private Point origin; + + // This represents the pre-normalized rectangle, which fits + // within the rectangle on the backing store. We keep a + // one-pixel border around entries on the backing store to + // prevent bleeding of adjacent letters when using GL_LINEAR + // filtering for rendering. The origin of this rectangle is + // equivalent to the origin above. + private Rectangle2D origRect; + private boolean used; // Whether this text was used recently - TextData(String str, Point origin, int unicodeID) { + TextData(String str, Point origin, Rectangle2D origRect, int unicodeID) { this.str = str; this.origin = origin; + this.origRect = origRect; this.unicodeID = unicodeID; } @@ -1216,6 +1103,20 @@ public class TextRenderer { return origin; } + // The following three methods are used to locate the glyph + // within the expanded rectangle coming from normalize() + int origOriginX() { + return (int) -origRect.getMinX(); + } + + int origOriginY() { + return (int) -origRect.getMinY(); + } + + Rectangle2D origRect() { + return origRect; + } + boolean used() { return used; } @@ -1276,8 +1177,13 @@ public class TextRenderer { if (attemptNumber == 0) { if (DEBUG) { System.err.println( - "Clearing unused entries in preExpand(): attempt number " + - attemptNumber); + "Clearing unused entries in preExpand(): attempt number " + + attemptNumber); + } + + if (inBeginEndPair) { + // Draw any outstanding glyphs + flush(); } clearUnusedEntries(); @@ -1397,7 +1303,7 @@ public class TextRenderer { public Rectangle2D getBounds(CharSequence str, Font font, FontRenderContext frc) { return getBounds(font.createGlyphVector(frc, - new MapCharSequenceToGlyphVector(str)), + new CharSequenceIterator(str)), frc); } @@ -1407,7 +1313,7 @@ public class TextRenderer { } public Rectangle2D getBounds(GlyphVector gv, FontRenderContext frc) { - return gv.getPixelBounds(frc, 0, 0); + return gv.getVisualBounds(); } public void drawGlyphVector(Graphics2D graphics, GlyphVector str, @@ -1423,278 +1329,358 @@ public class TextRenderer { //---------------------------------------------------------------------- // Glyph-by-glyph rendering support // - private static class DrawingState { - public static final int fast = 1; - public static final int robust = 2; - public static final int finished = 3; - } - class GlyphsUploadList { - int numberOfNewGlyphs; - GlyphVector[] glyphVector; - Rectangle2D[] glyphBounds; - int[] renderIndex; - int[] newGlyphs; - char[] newUnicodes; - - void prepGlyphForUpload(char inUnicodeID, int inGlyphID, - Rectangle2D inBounds, int inI, GlyphVector inGv) { - int slot = this.numberOfNewGlyphs; - - this.newUnicodes[slot] = inUnicodeID; - this.newGlyphs[slot] = inGlyphID; - this.glyphBounds[slot] = inBounds; - this.renderIndex[slot] = inI; - this.glyphVector[slot] = inGv; - this.numberOfNewGlyphs++; - } - - void uploadAnyNewGlyphs(GlyphsList outList, GlyphProducer mapper) { - for (int i = 0; i < this.numberOfNewGlyphs; i++) { - if (mapper.unicodes2Glyphs[this.newUnicodes[i]] == mapper.undefined) { - Rectangle2D bbox = normalize(this.glyphBounds[i]); - Point origin = new Point((int) -bbox.getMinX(), - (int) -bbox.getMinY()); - Rect rect = new Rect(0, 0, (int) bbox.getWidth(), - (int) bbox.getHeight(), - new TextData(null, origin, this.newUnicodes[i])); - GlyphVector gv = this.glyphVector[i]; - this.glyphVector[i] = null; // <--- dont need this anymore, so null it - - packer.add(rect); - - mapper.glyphRectForTextureMapping[this.newGlyphs[i]] = rect; - outList.textureSourceRect[this.renderIndex[i]] = rect; - mapper.unicodes2Glyphs[this.newUnicodes[i]] = this.newGlyphs[i]; // i do this here, so if i get two upload requests for same glyph, we handle it correctly - - Graphics2D g = getGraphics2D(); - - // OK, should now have an (x, y) for this rectangle; rasterize - // the String - // FIXME: need to verify that this causes the String to be - // rasterized fully into the bounding rectangle - int strx = rect.x() + origin.x; - int stry = rect.y() + origin.y; - - // ---1st frame performance gating factor--- - // Clear out the area we're going to draw into - // //-- only if we reuse backing store. Do we do this? If so, we should have a flag that says we need to clear? or do it in clearSpace itself - g.setComposite(AlphaComposite.Clear); - g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); - g.setComposite(AlphaComposite.Src); - - // Draw the string - renderDelegate.drawGlyphVector(g, gv, strx, stry); - - // Mark this region of the TextureRenderer as dirty - getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), - rect.h()); - } else { - outList.textureSourceRect[this.renderIndex[i]] = mapper.glyphRectForTextureMapping[this.newGlyphs[i]]; - } - } + // A temporary to prevent excessive garbage creation + private char[] singleUnicode = new char[1]; - this.numberOfNewGlyphs = 0; - } + /** A Glyph represents either a single unicode glyph or a + substring of characters to be drawn. The reason for the dual + behavior is so that we can take in a sequence of unicode + characters and partition them into runs of individual glyphs, + but if we encounter complex text and/or unicode sequences we + don't understand, we can render them using the + string-by-string method. <P> - public void allocateSpace(int inLength) { - int allocLength = Math.max(inLength, 100); + Glyphs need to be able to re-upload themselves to the backing + store on demand as we go along in the render sequence. + */ - if ((glyphVector == null) || (glyphVector.length < allocLength)) { - glyphVector = new GlyphVector[allocLength]; - glyphBounds = new Rectangle2D[allocLength]; - renderIndex = new int[allocLength]; - newGlyphs = new int[allocLength]; - newUnicodes = new char[allocLength]; - } + class Glyph { + // If this Glyph represents an individual unicode glyph, this + // is its unicode ID. If it represents a String, this is -1. + private int unicodeID; + // If the above field isn't -1, then these fields are used. + // The glyph code in the font + private int glyphCode; + // The GlyphProducer which created us + private GlyphProducer producer; + // The advance of this glyph + private float advance; + // The GlyphVector for this single character; this is passed + // in during construction but cleared during the upload + // process + private GlyphVector singleUnicodeGlyphVector; + // The rectangle of this glyph on the backing store, or null + // if it has been cleared due to space pressure + private Rect glyphRectForTextureMapping; + // If this Glyph represents a String, this is the sequence of + // characters + private String str; + // Whether we need a valid advance when rendering this string + // (i.e., whether it has other single glyphs coming after it) + private boolean needAdvance; + + // Creates a Glyph representing an individual Unicode character + public Glyph(int unicodeID, + int glyphCode, + float advance, + GlyphVector singleUnicodeGlyphVector, + GlyphProducer producer) { + this.unicodeID = unicodeID; + this.glyphCode = glyphCode; + this.advance = advance; + this.singleUnicodeGlyphVector = singleUnicodeGlyphVector; + this.producer = producer; } - } - - static class GlyphsList { - int /* DrawingState */ nextState; - CharSequence remaining; - float[] advances; - float totalAdvance; - Rect[] textureSourceRect; - int length; - public void allocateSpace(int inLength) { - int allocLength = Math.max(inLength, 100); - - if ((advances == null) || (advances.length < allocLength)) { - advances = new float[allocLength]; - textureSourceRect = new Rect[allocLength]; - } + // Creates a Glyph representing a sequence of characters, with + // an indication of whether additional single glyphs are being + // rendered after it + public Glyph(String str, boolean needAdvance) { + this.str = str; + this.needAdvance = needAdvance; } - } - class GlyphProducer { - final int undefined = -2; - final int needComplex = -1; - FontRenderContext fontRenderContext; - GlyphsList glyphsOutput = new GlyphsList(); - GlyphsUploadList glyphsToUpload = new GlyphsUploadList(); - char[] unicodes; - int[] unicodes2Glyphs; - char[] singleUnicode; - Rect[] glyphRectForTextureMapping; - float[] advances; - MapCharSequenceToGlyphVector iter = new MapCharSequenceToGlyphVector(); - char[] tempChars = new char[1]; - - GlyphProducer(FontRenderContext frc, int fontLengthInGlyphs) { - fontRenderContext = frc; - - if (advances == null) { - advances = new float[fontLengthInGlyphs]; - glyphRectForTextureMapping = new Rect[fontLengthInGlyphs]; - unicodes2Glyphs = new int[512]; - singleUnicode = new char[1]; - clearAllCacheEntries(); - } + /** Returns this glyph's unicode ID */ + public int getUnicodeID() { + return unicodeID; } - public void clearCacheEntry(int unicodeID) { - unicodes2Glyphs[unicodeID] = undefined; + /** Returns this glyph's (font-specific) glyph code */ + public int getGlyphCode() { + return glyphCode; } - public void clearAllCacheEntries() { - for (int i = 0; i < unicodes2Glyphs.length; i++) { - unicodes2Glyphs[i] = undefined; - } + /** Returns the advance for this glyph */ + public float getAdvance() { + return advance; } - public void allocateSpace(int length) { - length = Math.max(length, 100); + /** Draws this glyph and returns the (x) advance for this glyph */ + public float draw3D(float inX, float inY, float z, float scaleFactor) { + if (str != null) { + draw3D_ROBUST(str, inX, inY, z, scaleFactor); + if (!needAdvance) { + return 0; + } + // Compute and return the advance for this string + GlyphVector gv = font.createGlyphVector(getFontRenderContext(), str); + float totalAdvance = 0; + for (int i = 0; i < gv.getNumGlyphs(); i++) { + totalAdvance += gv.getGlyphMetrics(i).getAdvance(); + } + return totalAdvance; + } - if ((unicodes == null) || (unicodes.length < length)) { - unicodes = new char[length]; + // This is the code path taken for individual glyphs + if (glyphRectForTextureMapping == null) { + upload(); } - glyphsToUpload.allocateSpace(length); - glyphsOutput.allocateSpace(length); - } + try { + if (mPipelinedQuadRenderer == null) { + mPipelinedQuadRenderer = new Pipelined_QuadRenderer(); + } - float getGlyphPixelWidth(char unicodeID) { - int glyphID = undefined; + TextureRenderer renderer = getBackingStore(); + // Handles case where NPOT texture is used for backing store + TextureCoords wholeImageTexCoords = renderer.getTexture().getImageTexCoords(); + float xScale = wholeImageTexCoords.right(); + float yScale = wholeImageTexCoords.bottom(); - if (unicodeID < unicodes2Glyphs.length) // <--- could support the rare high unicode better later - { - glyphID = unicodes2Glyphs[unicodeID]; // Check to see if we have already encountered this unicode - } + Rect rect = glyphRectForTextureMapping; + TextData data = (TextData) rect.getUserData(); + data.markUsed(); - if (glyphID != undefined) // if we haven't, we must get some its attributes, and prep for upload - { - return advances[glyphID]; - } else { - tempChars[0] = unicodeID; + Rectangle2D origRect = data.origRect(); - GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext, - tempChars); + float x = inX - (scaleFactor * data.origOriginX()); + float y = inY - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())); - return fullRunGlyphVector.getGlyphMetrics(0).getAdvance(); - } + int texturex = rect.x() + (data.origin().x - data.origOriginX()); + int texturey = renderer.getHeight() - rect.y() - (int) origRect.getHeight() - + (data.origin().y - data.origOriginY()); + int width = (int) origRect.getWidth(); + int height = (int) origRect.getHeight(); - // return -1; - } + float tx1 = xScale * (float) texturex / (float) renderer.getWidth(); + float ty1 = yScale * (1.0f - + ((float) texturey / (float) renderer.getHeight())); + float tx2 = xScale * (float) (texturex + width) / (float) renderer.getWidth(); + float ty2 = yScale * (1.0f - + ((float) (texturey + height) / (float) renderer.getHeight())); - GlyphsList puntToRobust(CharSequence inString) { - glyphsOutput.nextState = DrawingState.robust; - glyphsOutput.remaining = inString; - // Reset the glyph uploader - glyphsToUpload.numberOfNewGlyphs = 0; - // Reset the glyph list - glyphsOutput.length = 0; - glyphsOutput.totalAdvance = 0; + mPipelinedQuadRenderer.glTexCoord2f(tx1, ty1); + mPipelinedQuadRenderer.glVertex3f(x, y, z); + mPipelinedQuadRenderer.glTexCoord2f(tx2, ty1); + mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), y, + z); + mPipelinedQuadRenderer.glTexCoord2f(tx2, ty2); + mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), + y + (height * scaleFactor), z); + mPipelinedQuadRenderer.glTexCoord2f(tx1, ty2); + mPipelinedQuadRenderer.glVertex3f(x, + y + (height * scaleFactor), z); + } catch (Exception e) { + e.printStackTrace(); + } + return advance; + } - return glyphsOutput; + /** Notifies this glyph that it's been cleared out of the cache */ + public void clear() { + glyphRectForTextureMapping = null; } - GlyphsList getGlyphs(CharSequence inString) { - float fontSize = font.getSize(); + private void upload() { + GlyphVector gv = getGlyphVector(); + Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(gv, getFontRenderContext())); + Rectangle2D bbox = normalize(origBBox); + Point origin = new Point((int) -bbox.getMinX(), + (int) -bbox.getMinY()); + Rect rect = new Rect(0, 0, (int) bbox.getWidth(), + (int) bbox.getHeight(), + new TextData(null, origin, origBBox, unicodeID)); + packer.add(rect); + glyphRectForTextureMapping = rect; + Graphics2D g = getGraphics2D(); + // OK, should now have an (x, y) for this rectangle; rasterize + // the glyph + int strx = rect.x() + origin.x; + int stry = rect.y() + origin.y; - if (fontSize > 128) { - glyphsOutput.nextState = DrawingState.robust; - glyphsOutput.remaining = inString; - } + // Clear out the area we're going to draw into + g.setComposite(AlphaComposite.Clear); + g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); + g.setComposite(AlphaComposite.Src); - int length = inString.length(); - allocateSpace(length); + // Draw the string + renderDelegate.drawGlyphVector(g, gv, strx, stry); - iter.initFromCharSequence(inString); + if (DRAW_BBOXES) { + TextData data = (TextData) rect.getUserData(); + // Draw a bounding box on the backing store + g.drawRect(strx - data.origOriginX(), + stry - data.origOriginY(), + (int) data.origRect().getWidth(), + (int) data.origRect().getHeight()); + g.drawRect(strx - data.origin().x, + stry - data.origin().y, + rect.w(), + rect.h()); + } - GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext, - iter); - boolean complex = (fullRunGlyphVector.getLayoutFlags() != 0); - int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs(); + // Mark this region of the TextureRenderer as dirty + getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), + rect.h()); + // Re-register ourselves with our producer + producer.register(this); + } - if (complex) { - return puntToRobust(inString); + private GlyphVector getGlyphVector() { + GlyphVector gv = singleUnicodeGlyphVector; + if (gv != null) { + singleUnicodeGlyphVector = null; // Don't need this anymore + return gv; } + singleUnicode[0] = (char) unicodeID; + return font.createGlyphVector(getFontRenderContext(), singleUnicode); + } + } - TextureRenderer renderer = getBackingStore(); - - float totalAdvanceUploaded = 0; - float cacheSize = renderer.getWidth() * renderer.getHeight(); + class GlyphProducer { + final int undefined = -2; + FontRenderContext fontRenderContext; + List/*<Glyph>*/ glyphsOutput = new ArrayList/*<Glyph>*/(); + // The mapping from unicode character to font-specific glyph ID + int[] unicodes2Glyphs; + // The mapping from glyph ID to Glyph + Glyph[] glyphCache; + // We re-use this for each incoming string + CharSequenceIterator iter = new CharSequenceIterator(); - for (int i = 0; i < lengthInGlyphs; i++) { - float advance; + GlyphProducer(int fontLengthInGlyphs) { + unicodes2Glyphs = new int[512]; + glyphCache = new Glyph[fontLengthInGlyphs]; + clearAllCacheEntries(); + } - char unicodeID = inString.charAt(i); + public List/*<Glyph>*/ getGlyphs(CharSequence inString) { + glyphsOutput.clear(); + iter.initFromCharSequence(inString); + GlyphVector fullRunGlyphVector = font.createGlyphVector(getFontRenderContext(), + iter); + boolean complex = (fullRunGlyphVector.getLayoutFlags() != 0); + if (complex || DISABLE_GLYPH_CACHE) { + // Punt to the robust version of the renderer + glyphsOutput.add(new Glyph(inString.toString(), false)); + return glyphsOutput; + } - if (unicodeID >= unicodes2Glyphs.length) { // <-- -could support these better - return puntToRobust(inString); + int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs(); + int i = 0; + while (i < lengthInGlyphs) { + Glyph glyph = getGlyph(inString, fullRunGlyphVector, i); + if (glyph != null) { + glyphsOutput.add(glyph); + i++; + } else { + // Assemble a run of characters that don't fit in + // the cache + StringBuffer buf = new StringBuffer(); + while (i < lengthInGlyphs && + getGlyph(inString, fullRunGlyphVector, i) == null) { + buf.append(inString.charAt(i++)); + } + glyphsOutput.add(new Glyph(buf.toString(), + // Any more glyphs after this run? + i < lengthInGlyphs - 1)); } + } + return glyphsOutput; + } - int glyphID = unicodes2Glyphs[unicodeID]; // Check to see if we have already encountered this unicode + public void clearCacheEntry(int unicodeID) { + int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + Glyph glyph = glyphCache[glyphID]; + if (glyph != null) { + glyph.clear(); + } + glyphCache[glyphID] = null; + } + unicodes2Glyphs[unicodeID] = undefined; + } - if (glyphID == undefined) { // if we haven't, we must get some its attributes, and prep for upload - GlyphMetrics metrics = fullRunGlyphVector.getGlyphMetrics(i); - singleUnicode[0] = unicodeID; + public void clearAllCacheEntries() { + for (int i = 0; i < unicodes2Glyphs.length; i++) { + clearCacheEntry(i); + } + } - GlyphVector gv = font.createGlyphVector(fontRenderContext, - singleUnicode); // need this to get single bitmaps - glyphID = gv.getGlyphCode(0); - // Have seen huge glyph codes (65536) coming out of some fonts in some Unicode situations - if (glyphID >= advances.length) { - return puntToRobust(inString); - } - advance = metrics.getAdvance(); - advances[glyphID] = advance; + public void register(Glyph glyph) { + unicodes2Glyphs[glyph.getUnicodeID()] = glyph.getGlyphCode(); + glyphCache[glyph.getGlyphCode()] = glyph; + } - glyphsToUpload.prepGlyphForUpload(unicodeID, glyphID, - renderDelegate.getBounds(gv, fontRenderContext), i, gv); + public float getGlyphPixelWidth(char unicodeID) { + Glyph glyph = getGlyph(unicodeID); + if (glyph != null) { + return glyph.getAdvance(); + } - totalAdvanceUploaded += advance; - } else { - Rect r = glyphRectForTextureMapping[glyphID]; - glyphsOutput.textureSourceRect[i] = r; + // Have to do this the hard / uncached way + singleUnicode[0] = unicodeID; + GlyphVector gv = font.createGlyphVector(fontRenderContext, + singleUnicode); + return gv.getGlyphMetrics(0).getAdvance(); + } - TextData data = (TextData) r.getUserData(); - data.markUsed(); + // Returns a glyph object for this single glyph. Returns null + // if the unicode or glyph ID would be out of bounds of the + // glyph cache. + private Glyph getGlyph(CharSequence inString, + GlyphVector fullRunGlyphVector, + int index) { + char unicodeID = inString.charAt(index); - advance = advances[glyphID]; - } + if (unicodeID >= unicodes2Glyphs.length) { + return null; + } - glyphsOutput.advances[i] = advance; - glyphsOutput.totalAdvance += advance; + int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + return glyphCache[glyphID]; + } - if ((totalAdvanceUploaded * fontSize) > (0.25f * cacheSize)) // note -- if the incoming string is bigger than 1/4 the total font cache, start segmenting glyph stream into bite sized pieces - { - glyphsToUpload.uploadAnyNewGlyphs(glyphsOutput, this); - glyphsOutput.length = i + 1; - glyphsOutput.remaining = inString.subSequence(i + 1, length); - glyphsOutput.nextState = DrawingState.fast; + // Must fabricate the glyph + singleUnicode[0] = unicodeID; + GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode); + return getGlyph(unicodeID, gv, fullRunGlyphVector.getGlyphMetrics(index)); + } - return glyphsOutput; - } + // It's unclear whether this variant might produce less + // optimal results than if we can see the entire GlyphVector + // for the incoming string + private Glyph getGlyph(int unicodeID) { + if (unicodeID >= unicodes2Glyphs.length) { + return null; } - glyphsOutput.length = lengthInGlyphs; - glyphsToUpload.uploadAnyNewGlyphs(glyphsOutput, this); - glyphsOutput.nextState = DrawingState.finished; - - return glyphsOutput; + int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + return glyphCache[glyphID]; + } + singleUnicode[0] = (char) unicodeID; + GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode); + return getGlyph(unicodeID, gv, gv.getGlyphMetrics(0)); + } + + private Glyph getGlyph(int unicodeID, + GlyphVector singleUnicodeGlyphVector, + GlyphMetrics metrics) { + int glyphCode = singleUnicodeGlyphVector.getGlyphCode(0); + // Have seen huge glyph codes (65536) coming out of some fonts in some Unicode situations + if (glyphCode >= glyphCache.length) { + return null; + } + Glyph glyph = new Glyph(unicodeID, + glyphCode, + metrics.getAdvance(), + singleUnicodeGlyphVector, + this); + register(glyph); + return glyph; } } @@ -1755,11 +1741,11 @@ public class TextRenderer { } private void draw() { - if (useVertexArrays) { - drawVertexArrays(); - } else { - drawIMMEDIATE(); - } + if (useVertexArrays) { + drawVertexArrays(); + } else { + drawIMMEDIATE(); + } } private void drawVertexArrays() { |