summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xsrc/classes/com/sun/opengl/util/j2d/TextRenderer.java938
-rwxr-xr-xtest/Issue344Base.java107
-rwxr-xr-xtest/Issue344Test1.java10
-rwxr-xr-xtest/Issue344Test2.java10
-rwxr-xr-xtest/Issue344Test3.java10
5 files changed, 599 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() {
diff --git a/test/Issue344Base.java b/test/Issue344Base.java
new file mode 100755
index 000000000..548e3ec21
--- /dev/null
+++ b/test/Issue344Base.java
@@ -0,0 +1,107 @@
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.event.*;
+import java.awt.geom.*;
+
+import javax.media.opengl.*;
+import javax.media.opengl.glu.*;
+import com.sun.opengl.util.*;
+import com.sun.opengl.util.j2d.*;
+
+/** Test Code adapted from TextCube.java (in JOGL demos)
+ *
+ * @author spiraljetty
+ * @author kbr
+ */
+
+public abstract class Issue344Base implements GLEventListener
+{
+ GLU glu = new GLU();
+ TextRenderer renderer;
+
+ float textScaleFactor;
+ Font font;
+ boolean useMipMaps;
+
+ protected Issue344Base() {
+ font = new Font("default", Font.PLAIN, 200);
+ useMipMaps = true; //false
+ }
+
+ protected abstract String getText();
+
+ protected void run(String[] args) {
+ Frame frame = new Frame(getClass().getName());
+ frame.setLayout(new BorderLayout());
+
+ GLCanvas canvas = new GLCanvas();
+ canvas.addGLEventListener(this);
+ frame.add(canvas, BorderLayout.CENTER);
+
+ frame.setSize(512, 512);
+ frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ new Thread(new Runnable() {
+ public void run() {
+ System.exit(0);
+ }
+ }).start();
+ }
+ });
+ frame.show();
+ }
+
+ public void init(GLAutoDrawable drawable)
+ {
+ GL gl = drawable.getGL();
+
+ gl.glEnable(GL.GL_DEPTH_TEST);
+
+ renderer = new TextRenderer(font, useMipMaps);
+
+ Rectangle2D bounds = renderer.getBounds(getText());
+ float w = (float) bounds.getWidth();
+ float h = (float) bounds.getHeight();
+ textScaleFactor = 2.0f / (w * 1.1f);
+ gl.setSwapInterval(0);
+ }
+
+ public void display(GLAutoDrawable drawable)
+ {
+ GL gl = drawable.getGL();
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
+
+ gl.glMatrixMode(GL.GL_MODELVIEW);
+ gl.glLoadIdentity();
+ glu.gluLookAt(0, 0, 10,
+ 0, 0, 0,
+ 0, 1, 0);
+
+ renderer.begin3DRendering();
+ Rectangle2D bounds = renderer.getBounds(getText());
+ float w = (float) bounds.getWidth();
+ float h = (float) bounds.getHeight();
+ renderer.draw3D(getText(),
+ w / -2.0f * textScaleFactor,
+ h / -2.0f * textScaleFactor,
+ 3f,
+ textScaleFactor);
+
+ renderer.end3DRendering();
+ }
+
+ public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height)
+ {
+ GL gl = drawable.getGL();
+ gl.glMatrixMode(GL.GL_PROJECTION);
+ gl.glLoadIdentity();
+ glu.gluPerspective(15, (float) width / (float) height, 5, 15);
+ }
+
+ public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
+ boolean deviceChanged)
+ {
+ }
+}
diff --git a/test/Issue344Test1.java b/test/Issue344Test1.java
new file mode 100755
index 000000000..c0608ed6e
--- /dev/null
+++ b/test/Issue344Test1.java
@@ -0,0 +1,10 @@
+public class Issue344Test1 extends Issue344Base {
+ protected String getText() {
+ // test 1 - weird artifacts appear with a large font & long string
+ return "abcdefghijklmnopqrstuvwxyz1234567890";
+ }
+
+ public static void main(String[] args) {
+ new Issue344Test1().run(args);
+ }
+}
diff --git a/test/Issue344Test2.java b/test/Issue344Test2.java
new file mode 100755
index 000000000..b0900438c
--- /dev/null
+++ b/test/Issue344Test2.java
@@ -0,0 +1,10 @@
+public class Issue344Test2 extends Issue344Base {
+ protected String getText() {
+ // test 2 - unicode hangs program with a large font & long string
+ return "\u201Cabcdefghijklmnopqrstuvwxyz\u201D";
+ }
+
+ public static void main(String[] args) {
+ new Issue344Test2().run(args);
+ }
+}
diff --git a/test/Issue344Test3.java b/test/Issue344Test3.java
new file mode 100755
index 000000000..381bf0a1c
--- /dev/null
+++ b/test/Issue344Test3.java
@@ -0,0 +1,10 @@
+public class Issue344Test3 extends Issue344Base {
+ protected String getText() {
+ // test 3 - slight rendering artifacts around very large letters
+ return "abcde";
+ }
+
+ public static void main(String[] args) {
+ new Issue344Test3().run(args);
+ }
+}