diff options
Diffstat (limited to 'src/classes/com')
-rwxr-xr-x | src/classes/com/sun/opengl/util/j2d/TextRenderer.java | 2948 |
1 files changed, 1491 insertions, 1457 deletions
diff --git a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java index 473390ccf..312c99c13 100755 --- a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java +++ b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java @@ -1,21 +1,21 @@ /* * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: - * + * * - Redistribution of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * + * * - Redistribution in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * + * * Neither the name of Sun Microsystems, Inc. or the names of * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. - * + * * This software is provided "AS IS," without a warranty of any kind. ALL * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A @@ -28,42 +28,46 @@ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - * + * * You acknowledge that this software is not designed or intended for use * in the design, construction, operation or maintenance of any nuclear * facility. - * + * * Sun gratefully acknowledges that this software was originally authored * and developed by Kenneth Bradley Russell and Christopher John Kline. */ - package com.sun.opengl.util.j2d; +import com.sun.opengl.impl.*; +import com.sun.opengl.impl.packrect.*; +import com.sun.opengl.util.*; +import com.sun.opengl.util.texture.*; + import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; + +// For debugging purposes +import java.awt.EventQueue; import java.awt.Font; +import java.awt.Frame; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.RenderingHints; +import java.awt.event.*; import java.awt.font.*; import java.awt.geom.*; + import java.nio.*; + import java.text.*; + import java.util.*; import javax.media.opengl.*; import javax.media.opengl.glu.*; -import com.sun.opengl.impl.packrect.*; -// For debugging purposes -import java.awt.EventQueue; -import java.awt.Frame; -import java.awt.event.*; -import com.sun.opengl.impl.*; -import com.sun.opengl.util.*; -import com.sun.opengl.util.texture.*; /** Renders bitmapped Java 2D text into an OpenGL window with high performance, full Unicode support, and a simple API. Performs @@ -118,1624 +122,1654 @@ import com.sun.opengl.util.texture.*; @author John Burkey @author Kenneth Russell */ - -public class TextRenderer -{ - private static final boolean DEBUG = Debug.debug("TextRenderer"); - - static final int kSize = 256; - - private Font font; - private boolean antialiased; - private boolean useFractionalMetrics; - - // Whether we're attempting to use automatic mipmap generation support - private boolean mipmap; - - private RectanglePacker packer; - private boolean haveMaxSize; - private RenderDelegate renderDelegate; - private TextureRenderer cachedBackingStore; - private Graphics2D cachedGraphics; - private FontRenderContext cachedFontRenderContext; - 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/*<String>*/ tokenizationResults = new ArrayList/*<String>*/(); - - // Every certain number of render cycles, flush the strings which - // haven't been used recently - private static final int CYCLES_PER_FLUSH = 100; - private int numRenderCycles; - // The amount of vertical dead space on the backing store before we - // force a compaction - private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f; - - private static class MapCharSequenceToGlyphVector - implements CharacterIterator - { - CharSequence mSequence; - int mLength; - int mCurrentIndex; - - MapCharSequenceToGlyphVector() - { - +public class TextRenderer { + private static final boolean DEBUG = Debug.debug("TextRenderer"); + static final int kSize = 256; + + // Every certain number of render cycles, flush the strings which + // haven't been used recently + private static final int CYCLES_PER_FLUSH = 100; + + // The amount of vertical dead space on the backing store before we + // force a compaction + private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f; + static final int kQuadsPerBuffer = 100; + static final int kCoordsPerVertVerts = 3; + static final int kCoordsPerVertTex = 2; + static final int kVertsPerQuad = 4; + static final int kTotalBufferSizeVerts = kQuadsPerBuffer * kVertsPerQuad; + static final int kTotalBufferSizeCoordsVerts = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertVerts; + static final int kTotalBufferSizeCoordsTex = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertTex; + static final int kTotalBufferSizeBytesVerts = kTotalBufferSizeCoordsVerts * 4; + static final int kTotalBufferSizeBytesTex = kTotalBufferSizeCoordsTex * 4; + static final int kSizeInBytes_OneVertices_VertexData = kCoordsPerVertVerts * 4; + static final int kSizeInBytes_OneVertices_TexData = kCoordsPerVertTex * 4; + private Font font; + private boolean antialiased; + private boolean useFractionalMetrics; + + // Whether we're attempting to use automatic mipmap generation support + private boolean mipmap; + private RectanglePacker packer; + private boolean haveMaxSize; + private RenderDelegate renderDelegate; + private TextureRenderer cachedBackingStore; + private Graphics2D cachedGraphics; + private FontRenderContext cachedFontRenderContext; + 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() / + // endRendering() cycle so we can re-enter the exact same state if + // we have to reallocate the backing store + private boolean inBeginEndPair; + private boolean isOrthoMode; + private int beginRenderingWidth; + private int beginRenderingHeight; + private boolean beginRenderingDepthTestDisabled; + + // For resetting the color after disposal of the old backing store + private boolean haveCachedColor; + private float cachedR; + private float cachedG; + private float cachedB; + private float cachedA; + private Color cachedColor; + private boolean needToResetColor; + + // For debugging only + private Frame dbgFrame; + + // Debugging purposes only + private boolean debugged; + Pipelined_QuadRenderer mPipelinedQuadRenderer; + + /** Creates a new TextRenderer with the given font, using no + antialiasing or fractional metrics, and the default + RenderDelegate. Equivalent to <code>TextRenderer(font, false, + false)</code>. + + @param font the font to render with + */ + public TextRenderer(Font font) { + this(font, false, false, null, false); } - MapCharSequenceToGlyphVector(CharSequence sequence) - { - initFromCharSequence(sequence); + /** Creates a new TextRenderer with the given font, using no + antialiasing or fractional metrics, and the default + RenderDelegate. If <CODE>mipmap</CODE> is true, attempts to use + OpenGL's automatic mipmap generation for better smoothing when + rendering the TextureRenderer's contents at a distance. + Equivalent to <code>TextRenderer(font, false, false)</code>. + + @param font the font to render with + @param mipmap whether to attempt use of automatic mipmap generation + */ + public TextRenderer(Font font, boolean mipmap) { + this(font, false, false, null, mipmap); } - public void initFromCharSequence(CharSequence sequence) - { - mSequence = sequence; - mLength = mSequence.length (); - mCurrentIndex = 0; + /** Creates a new TextRenderer with the given Font, specified font + properties, and default RenderDelegate. The + <code>antialiased</code> and <code>useFractionalMetrics</code> + flags provide control over the same properties at the Java 2D + level. No mipmap support is requested. Equivalent to + <code>TextRenderer(font, antialiased, useFractionalMetrics, + null)</code>. + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + */ + public TextRenderer(Font font, boolean antialiased, + boolean useFractionalMetrics) { + this(font, antialiased, useFractionalMetrics, null, false); } - public char last () - { - mCurrentIndex = Math.max(0,mLength-1); - return current(); + /** Creates a new TextRenderer with the given Font, specified font + properties, and given RenderDelegate. The + <code>antialiased</code> and <code>useFractionalMetrics</code> + flags provide control over the same properties at the Java 2D + level. The <code>renderDelegate</code> provides more control + over the text rendered. No mipmap support is requested. + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + @param renderDelegate the render delegate to use to draw the + text's bitmap, or null to use the default one + */ + public TextRenderer(Font font, boolean antialiased, + boolean useFractionalMetrics, RenderDelegate renderDelegate) { + this(font, antialiased, useFractionalMetrics, renderDelegate, false); } - public char current () - { - if (mLength == 0 || mCurrentIndex >= mLength) - { - return CharacterIterator.DONE; + /** Creates a new TextRenderer with the given Font, specified font + properties, and given RenderDelegate. The + <code>antialiased</code> and <code>useFractionalMetrics</code> + flags provide control over the same properties at the Java 2D + level. The <code>renderDelegate</code> provides more control + over the text rendered. If <CODE>mipmap</CODE> is true, attempts + to use OpenGL's automatic mipmap generation for better smoothing + when rendering the TextureRenderer's contents at a distance. + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + @param renderDelegate the render delegate to use to draw the + text's bitmap, or null to use the default one + @param mipmap whether to attempt use of automatic mipmap generation + */ + public TextRenderer(Font font, boolean antialiased, + boolean useFractionalMetrics, RenderDelegate renderDelegate, + boolean mipmap) { + this.font = font; + this.antialiased = antialiased; + this.useFractionalMetrics = useFractionalMetrics; + this.mipmap = mipmap; + + // FIXME: consider adjusting the size based on font size + // (it will already automatically resize if necessary) + packer = new RectanglePacker(new Manager(), kSize, kSize); + + if (renderDelegate == null) { + renderDelegate = new DefaultRenderDelegate(); } - return mSequence.charAt (mCurrentIndex); + this.renderDelegate = renderDelegate; + + mGlyphProducer = new GlyphProducer(getFontRenderContext(), + font.getNumGlyphs()); } - public char next () - { - mCurrentIndex++; - return current(); + /** Returns the bounding rectangle of the given String, assuming it + was rendered at the origin. See {@link #getBounds(CharSequence) + getBounds(CharSequence)}. */ + public Rectangle2D getBounds(String str) { + return getBounds((CharSequence) str); } - public char previous () - { - mCurrentIndex = Math.max(mCurrentIndex-1, 0); + /** Returns the bounding rectangle of the given CharSequence, + assuming it was rendered at the origin. The coordinate system of + the returned rectangle is Java 2D's, with increasing Y + coordinates in the downward direction. The relative coordinate + (0, 0) in the returned rectangle corresponds to the baseline of + the leftmost character of the rendered string, in similar + fashion to the results returned by, for example, {@link + java.awt.font.GlyphVector#getVisualBounds}. Most applications + will use only the width and height of the returned Rectangle for + the purposes of centering or justifying the String. It is not + specified which Java 2D bounds ({@link + java.awt.font.GlyphVector#getVisualBounds getVisualBounds}, + {@link java.awt.font.GlyphVector#getPixelBounds getPixelBounds}, + 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 + Rect r = null; + + if ((r = (Rect) stringLocations.get(str)) != null) { + TextData data = (TextData) r.getUserData(); + + // Reconstitute the Java 2D results based on the cached values + return new Rectangle2D.Double(-data.origin().x, -data.origin().y, + r.w(), r.h()); + } - return current(); + // Must return a Rectangle compatible with the layout algorithm -- + // must be idempotent + return normalize(renderDelegate.getBounds(str, font, + getFontRenderContext())); } - public char setIndex ( int position ) - { - mCurrentIndex = position; - return current(); + /** Returns the Font this renderer is using. */ + public Font getFont() { + return font; } - public int getBeginIndex () - { - return 0; + /** Returns a FontRenderContext which can be used for external + text-related size computations. This object should be considered + transient and may become invalidated between {@link + #beginRendering beginRendering} / {@link #endRendering + endRendering} pairs. */ + public FontRenderContext getFontRenderContext() { + if (cachedFontRenderContext == null) { + cachedFontRenderContext = getGraphics2D().getFontRenderContext(); + } + + return cachedFontRenderContext; } - public int getEndIndex () - { - return mLength; + /** Begins rendering with this {@link TextRenderer TextRenderer} + into the current OpenGL drawable, pushing the projection and + modelview matrices and some state bits and setting up a + two-dimensional orthographic projection with (0, 0) as the + lower-left coordinate and (width, height) as the upper-right + coordinate. Binds and enables the internal OpenGL texture + object, sets the texture environment mode to GL_MODULATE, and + changes the current color to the last color set with this + TextRenderer via {@link #setColor setColor}. This method + disables the depth test and is equivalent to + beginRendering(width, height, true). + + @param width the width of the current on-screen OpenGL drawable + @param height the height of the current on-screen OpenGL drawable + @throws javax.media.opengl.GLException If an OpenGL context is not current when this method is called + */ + public void beginRendering(int width, int height) throws GLException { + beginRendering(width, height, true); } - public int getIndex () - { - return mCurrentIndex; + /** Begins rendering with this {@link TextRenderer TextRenderer} + into the current OpenGL drawable, pushing the projection and + modelview matrices and some state bits and setting up a + two-dimensional orthographic projection with (0, 0) as the + lower-left coordinate and (width, height) as the upper-right + coordinate. Binds and enables the internal OpenGL texture + object, sets the texture environment mode to GL_MODULATE, and + changes the current color to the last color set with this + TextRenderer via {@link #setColor setColor}. Disables the depth + test if the disableDepthTest argument is true. + + @param width the width of the current on-screen OpenGL drawable + @param height the height of the current on-screen OpenGL drawable + @param disableDepthTest whether to disable the depth test + @throws GLException If an OpenGL context is not current when this method is called + */ + public void beginRendering(int width, int height, boolean disableDepthTest) + throws GLException { + beginRendering(true, width, height, disableDepthTest); } - public Object clone () - { - MapCharSequenceToGlyphVector iter = new MapCharSequenceToGlyphVector(mSequence); - iter.mCurrentIndex = mCurrentIndex; - return iter; + /** Begins rendering of 2D text in 3D with this {@link TextRenderer + TextRenderer} into the current OpenGL drawable. Assumes the end + user is responsible for setting up the modelview and projection + matrices, and will render text using the {@link #draw3D draw3D} + method. This method pushes some OpenGL state bits, binds and + enables the internal OpenGL texture object, sets the texture + environment mode to GL_MODULATE, and changes the current color + to the last color set with this TextRenderer via {@link + #setColor setColor}. + + @throws GLException If an OpenGL context is not current when this method is called + */ + public void begin3DRendering() throws GLException { + beginRendering(false, 0, 0, false); } - public char first () - { - if (mLength == 0) - { - return CharacterIterator.DONE; + /** Changes the current color of this TextRenderer to the supplied + one. The default color is opaque white. + + @param color the new color to use for rendering text + @throws GLException If an OpenGL context is not current when this method is called + */ + public void setColor(Color color) throws GLException { + boolean noNeedForFlush = (haveCachedColor && (cachedColor != null) && + color.equals(cachedColor)); + + if (!noNeedForFlush) { + flushGlyphPipeline(); } - mCurrentIndex = 0; - return current(); - } - } - - // Data associated with each rectangle of text - static class TextData { - 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 - // 2D coordinate system) at which the string must be rasterized in - // order to fit within the rectangle -- the leftmost point of the - // baseline. - private Point origin; - private boolean used; // Whether this text was used recently - - TextData(String str, Point origin, int unicodeID) { - this.str = str; - this.origin = origin; - this.unicodeID = unicodeID; + getBackingStore().setColor(color); + haveCachedColor = true; + cachedColor = color; } - String string() { return str; } - Point origin() { return origin; } - boolean used() { return used; } - void markUsed() { used = true; } - void clearUsed() { used = false; } - } - - // Need to keep track of whether we're in a beginRendering() / - // endRendering() cycle so we can re-enter the exact same state if - // we have to reallocate the backing store - private boolean inBeginEndPair; - private boolean isOrthoMode; - private int beginRenderingWidth; - private int beginRenderingHeight; - private boolean beginRenderingDepthTestDisabled; - // For resetting the color after disposal of the old backing store - private boolean haveCachedColor; - private float cachedR; - private float cachedG; - private float cachedB; - private float cachedA; - private Color cachedColor; - private boolean needToResetColor; - - // For debugging only - private Frame dbgFrame; - - /** Class supporting more full control over the process of rendering - the bitmapped text. Allows customization of whether the backing - store text bitmap is full-color or intensity only, the size of - each individual rendered text rectangle, and the contents of - each individual rendered text string. The default implementation - of this interface uses an intensity-only texture, a - closely-cropped rectangle around the text, and renders text - using the color white, which is modulated by the set color - during the rendering process. */ - public static interface RenderDelegate { - /** Indicates whether the backing store of this TextRenderer - should be intensity-only (the default) or full-color. */ - public boolean intensityOnly(); - - /** Computes the bounds of the given String relative to the - origin. */ - public Rectangle2D getBounds(String str, - Font font, - FontRenderContext frc); - - /** Computes the bounds of the given character sequence relative - to the origin. */ - public Rectangle2D getBounds(CharSequence str, - Font font, - FontRenderContext frc); - - /** Render the passed character sequence at the designated - location using the supplied Graphics2D instance. The - surrounding region will already have been cleared to the RGB - color (0, 0, 0) with zero alpha. The initial drawing context - of the passed Graphics2D will be set to use - AlphaComposite.Src, the color white, the Font specified in the - TextRenderer's constructor, and the rendering hints specified - in the TextRenderer constructor. Changes made by the end user - may be visible in successive calls to this method, but are not - guaranteed to be preserved. Implementors of this method - should reset the Graphics2D's state to that desired each time - this method is called, in particular those states which are - not the defaults. */ - public void draw(Graphics2D graphics, String str, int x, int y); - - /** Render the passed GlyphVector at the designated location using - the supplied Graphics2D instance. The surrounding region will - already have been cleared to the RGB color (0, 0, 0) with zero - alpha. The initial drawing context of the passed Graphics2D - will be set to use AlphaComposite.Src, the color white, the - Font specified in the TextRenderer's constructor, and the - rendering hints specified in the TextRenderer constructor. - Changes made by the end user may be visible in successive - calls to this method, but are not guaranteed to be preserved. - Implementors of this method should reset the Graphics2D's - state to that desired each time this method is called, in - particular those states which are not the defaults. */ - public void drawGlyphVector(Graphics2D graphics, GlyphVector str, int x, int y); - } - - // Debugging purposes only - private boolean debugged; - - /** Creates a new TextRenderer with the given font, using no - antialiasing or fractional metrics, and the default - RenderDelegate. Equivalent to <code>TextRenderer(font, false, - false)</code>. - - @param font the font to render with - */ - public TextRenderer (Font font) { - this(font, false, false, null, false); - } - - /** Creates a new TextRenderer with the given font, using no - antialiasing or fractional metrics, and the default - RenderDelegate. If <CODE>mipmap</CODE> is true, attempts to use - OpenGL's automatic mipmap generation for better smoothing when - rendering the TextureRenderer's contents at a distance. - Equivalent to <code>TextRenderer(font, false, false)</code>. - - @param font the font to render with - @param mipmap whether to attempt use of automatic mipmap generation - */ - public TextRenderer (Font font, boolean mipmap) { - this(font, false, false, null, mipmap); - } - - /** Creates a new TextRenderer with the given Font, specified font - properties, and default RenderDelegate. The - <code>antialiased</code> and <code>useFractionalMetrics</code> - flags provide control over the same properties at the Java 2D - level. No mipmap support is requested. Equivalent to - <code>TextRenderer(font, antialiased, useFractionalMetrics, - null)</code>. - - @param font the font to render with - @param antialiased whether to use antialiased fonts - @param useFractionalMetrics whether to use fractional font - metrics at the Java 2D level - */ - public TextRenderer (Font font, - boolean antialiased, - boolean useFractionalMetrics) { - this(font, antialiased, useFractionalMetrics, null, false); - } - - /** Creates a new TextRenderer with the given Font, specified font - properties, and given RenderDelegate. The - <code>antialiased</code> and <code>useFractionalMetrics</code> - flags provide control over the same properties at the Java 2D - level. The <code>renderDelegate</code> provides more control - over the text rendered. No mipmap support is requested. - - @param font the font to render with - @param antialiased whether to use antialiased fonts - @param useFractionalMetrics whether to use fractional font - metrics at the Java 2D level - @param renderDelegate the render delegate to use to draw the - text's bitmap, or null to use the default one - */ - public TextRenderer (Font font, - boolean antialiased, - boolean useFractionalMetrics, - RenderDelegate renderDelegate) { - this(font, antialiased, useFractionalMetrics, renderDelegate, false); - } - - /** Creates a new TextRenderer with the given Font, specified font - properties, and given RenderDelegate. The - <code>antialiased</code> and <code>useFractionalMetrics</code> - flags provide control over the same properties at the Java 2D - level. The <code>renderDelegate</code> provides more control - over the text rendered. If <CODE>mipmap</CODE> is true, attempts - to use OpenGL's automatic mipmap generation for better smoothing - when rendering the TextureRenderer's contents at a distance. - - @param font the font to render with - @param antialiased whether to use antialiased fonts - @param useFractionalMetrics whether to use fractional font - metrics at the Java 2D level - @param renderDelegate the render delegate to use to draw the - text's bitmap, or null to use the default one - @param mipmap whether to attempt use of automatic mipmap generation - */ - public TextRenderer (Font font, - boolean antialiased, - boolean useFractionalMetrics, - RenderDelegate renderDelegate, - boolean mipmap) { - this.font = font; - this.antialiased = antialiased; - this.useFractionalMetrics = useFractionalMetrics; - this.mipmap = mipmap; - - // FIXME: consider adjusting the size based on font size - // (it will already automatically resize if necessary) - packer = new RectanglePacker(new Manager(), kSize, kSize); - - if (renderDelegate == null) - { - renderDelegate = new DefaultRenderDelegate(); - } - - this.renderDelegate = renderDelegate; - - mGlyphProducer = new GlyphProducer (getFontRenderContext(),font.getNumGlyphs ()); - } - - /** Returns the bounding rectangle of the given String, assuming it - was rendered at the origin. See {@link #getBounds(CharSequence) - getBounds(CharSequence)}. */ - public Rectangle2D getBounds(String str) - { - return getBounds((CharSequence) str); - } - - /** Returns the bounding rectangle of the given CharSequence, - assuming it was rendered at the origin. The coordinate system of - the returned rectangle is Java 2D's, with increasing Y - coordinates in the downward direction. The relative coordinate - (0, 0) in the returned rectangle corresponds to the baseline of - the leftmost character of the rendered string, in similar - fashion to the results returned by, for example, {@link - java.awt.font.GlyphVector#getVisualBounds}. Most applications - will use only the width and height of the returned Rectangle for - the purposes of centering or justifying the String. It is not - specified which Java 2D bounds ({@link - java.awt.font.GlyphVector#getVisualBounds getVisualBounds}, - {@link java.awt.font.GlyphVector#getPixelBounds getPixelBounds}, - 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 - Rect r = null; - if ((r = (Rect) stringLocations.get(str)) != null) { - TextData data = (TextData) r.getUserData(); - // Reconstitute the Java 2D results based on the cached values - return new Rectangle2D.Double(-data.origin().x, - -data.origin().y, - r.w(), r.h()); - } + /** Changes the current color of this TextRenderer to the supplied + one, where each component ranges from 0.0f - 1.0f. The alpha + component, if used, does not need to be premultiplied into the + color channels as described in the documentation for {@link + com.sun.opengl.util.texture.Texture Texture}, although + premultiplied colors are used internally. The default color is + opaque white. + + @param r the red component of the new color + @param g the green component of the new color + @param b the blue component of the new color + @param a the alpha component of the new color, 0.0f = completely + transparent, 1.0f = completely opaque + @throws GLException If an OpenGL context is not current when this method is called + */ + public void setColor(float r, float g, float b, float a) + throws GLException { + boolean noNeedForFlush = (haveCachedColor && (cachedColor == null) && + (r == cachedR) && (g == cachedG) && (b == cachedB) && + (a == cachedA)); + + if (!noNeedForFlush) { + flushGlyphPipeline(); + } - // Must return a Rectangle compatible with the layout algorithm -- - // must be idempotent - return normalize(renderDelegate.getBounds(str, font, getFontRenderContext())); - } - - /** Returns the Font this renderer is using. */ - public Font getFont() { - return font; - } - - /** Returns a FontRenderContext which can be used for external - text-related size computations. This object should be considered - transient and may become invalidated between {@link - #beginRendering beginRendering} / {@link #endRendering - endRendering} pairs. */ - public FontRenderContext getFontRenderContext() { - if (cachedFontRenderContext == null) { - cachedFontRenderContext = getGraphics2D().getFontRenderContext(); - } - return cachedFontRenderContext; - } - - /** Begins rendering with this {@link TextRenderer TextRenderer} - into the current OpenGL drawable, pushing the projection and - modelview matrices and some state bits and setting up a - two-dimensional orthographic projection with (0, 0) as the - lower-left coordinate and (width, height) as the upper-right - coordinate. Binds and enables the internal OpenGL texture - object, sets the texture environment mode to GL_MODULATE, and - changes the current color to the last color set with this - TextRenderer via {@link #setColor setColor}. This method - disables the depth test and is equivalent to - beginRendering(width, height, true). - - @param width the width of the current on-screen OpenGL drawable - @param height the height of the current on-screen OpenGL drawable - @throws javax.media.opengl.GLException If an OpenGL context is not current when this method is called - */ - public void beginRendering(int width, int height) throws GLException - { - beginRendering(width, height, true); - } - - /** Begins rendering with this {@link TextRenderer TextRenderer} - into the current OpenGL drawable, pushing the projection and - modelview matrices and some state bits and setting up a - two-dimensional orthographic projection with (0, 0) as the - lower-left coordinate and (width, height) as the upper-right - coordinate. Binds and enables the internal OpenGL texture - object, sets the texture environment mode to GL_MODULATE, and - changes the current color to the last color set with this - TextRenderer via {@link #setColor setColor}. Disables the depth - test if the disableDepthTest argument is true. - - @param width the width of the current on-screen OpenGL drawable - @param height the height of the current on-screen OpenGL drawable - @param disableDepthTest whether to disable the depth test - @throws GLException If an OpenGL context is not current when this method is called - */ - public void beginRendering(int width, int height, boolean disableDepthTest) throws GLException { - beginRendering(true, width, height, disableDepthTest); - } - - /** Begins rendering of 2D text in 3D with this {@link TextRenderer - TextRenderer} into the current OpenGL drawable. Assumes the end - user is responsible for setting up the modelview and projection - matrices, and will render text using the {@link #draw3D draw3D} - method. This method pushes some OpenGL state bits, binds and - enables the internal OpenGL texture object, sets the texture - environment mode to GL_MODULATE, and changes the current color - to the last color set with this TextRenderer via {@link - #setColor setColor}. - - @throws GLException If an OpenGL context is not current when this method is called - */ - public void begin3DRendering() throws GLException { - beginRendering(false, 0, 0, false); - } - - /** Changes the current color of this TextRenderer to the supplied - one. The default color is opaque white. - - @param color the new color to use for rendering text - @throws GLException If an OpenGL context is not current when this method is called - */ - - public void setColor(Color color) throws GLException - { - boolean noNeedForFlush = (haveCachedColor && cachedColor != null && color.equals ( cachedColor)); - if (!noNeedForFlush) - { - flushGlyphPipeline (); - } - - getBackingStore().setColor(color); - haveCachedColor = true; - cachedColor = color; - } - - /** Changes the current color of this TextRenderer to the supplied - one, where each component ranges from 0.0f - 1.0f. The alpha - component, if used, does not need to be premultiplied into the - color channels as described in the documentation for {@link - com.sun.opengl.util.texture.Texture Texture}, although - premultiplied colors are used internally. The default color is - opaque white. - - @param r the red component of the new color - @param g the green component of the new color - @param b the blue component of the new color - @param a the alpha component of the new color, 0.0f = completely - transparent, 1.0f = completely opaque - @throws GLException If an OpenGL context is not current when this method is called - */ - public void setColor(float r, float g, float b, float a) throws GLException - { - boolean noNeedForFlush = (haveCachedColor && - cachedColor == null && - r == cachedR && - g == cachedG && - b == cachedB && - a == cachedA); - if (!noNeedForFlush) - { - flushGlyphPipeline (); - } - - getBackingStore().setColor(r, g, b, a); - haveCachedColor = true; - cachedR = r; - cachedG = g; - cachedB = b; - cachedA = a; - cachedColor = null; - } - - /** Draws the supplied CharSequence at the desired location using - the renderer's current color. The baseline of the leftmost - character is at position (x, y) specified in OpenGL coordinates, - where the origin is at the lower-left of the drawable and the Y - coordinate increases in the upward direction. - - @param str the string to draw - @param x the x coordinate at which to draw - @param y the y coordinate at which to draw - @throws GLException If an OpenGL context is not current when this method is called - */ - public void draw(CharSequence str, int x, int y) throws GLException { - draw3D(str, x, y, 0, 1); - } - - /** Draws the supplied String at the desired location using the - renderer's current color. See {@link #draw(CharSequence, int, - int) draw(CharSequence, int, int)}. */ - public void draw(String str, int x, int y) throws GLException { - draw3D(str, x, y, 0, 1); - } - - /** Draws the supplied CharSequence at the desired 3D location using - the renderer's current color. The baseline of the leftmost - character is placed at position (x, y, z) in the current - coordinate system. - - @param str the string to draw - @param x the x coordinate at which to draw - @param y the y coordinate at which to draw - @param z the z coordinate at which to draw - @param scaleFactor a uniform scale factor applied to the width and height of the drawn rectangle - @throws GLException If an OpenGL context is not current when this method is called - */ - public void draw3D(CharSequence str,float x, float y, float z,float scaleFactor) - { - internal_draw3D(str,x, y, z,scaleFactor); - } - - /** Draws the supplied String at the desired 3D location using the - renderer's current color. See {@link #draw3D(CharSequence, - float, float, float, float) draw3D(CharSequence, float, float, - float, float)}. */ - public void draw3D(String str,float x, float y, float z,float scaleFactor) - { - internal_draw3D(str,x, y, z,scaleFactor); - } - - /** Returns the pixel width of the given character. */ - public float getCharWidth(char inChar) - { - return mGlyphProducer.getGlyphPixelWidth ( inChar); - } - - /** Causes the TextRenderer to flush any internal caches it may be - maintaining and draw its rendering results to the screen. This - should be called after each call to draw() if you are setting - OpenGL state such as the modelview matrix between calls to - draw(). */ - public void flush() { - flushGlyphPipeline(); - } - - /** Ends a render cycle with this {@link TextRenderer TextRenderer}. - Restores the projection and modelview matrices as well as - several OpenGL state bits. Should be paired with {@link - #beginRendering beginRendering}. - - @throws GLException If an OpenGL context is not current when this method is called - */ - public void endRendering() throws GLException { - 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(); + getBackingStore().setColor(r, g, b, a); + haveCachedColor = true; + cachedR = r; + cachedG = g; + cachedB = b; + cachedA = a; + cachedColor = null; } - return spaceWidth; - } - - /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}. - Restores several OpenGL state bits. Should be paired with {@link - #begin3DRendering begin3DRendering}. - - @throws GLException If an OpenGL context is not current when this method is called - */ - public void end3DRendering() throws GLException { - endRendering(false); - } - - /** Disposes of all resources this TextRenderer is using. It is not - valid to use the TextRenderer after this method is called. - - @throws GLException If an OpenGL context is not current when this method is called - */ - public void dispose() throws GLException { - packer.dispose(); - packer = null; - cachedBackingStore = null; - cachedGraphics = null; - cachedFontRenderContext = null; - if (dbgFrame != null) { - dbgFrame.dispose(); - } - } - - //---------------------------------------------------------------------- - // 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 TextureRenderer getBackingStore() { - TextureRenderer renderer = (TextureRenderer) packer.getBackingStore(); - if (renderer != cachedBackingStore) { - // Backing store changed since last time; discard any cached Graphics2D - if (cachedGraphics != null) { - cachedGraphics.dispose(); - cachedGraphics = null; - cachedFontRenderContext = null; - } - cachedBackingStore = renderer; - } - return cachedBackingStore; - } - - private Graphics2D getGraphics2D() { - TextureRenderer renderer = getBackingStore(); - if (cachedGraphics == null) { - cachedGraphics = renderer.createGraphics(); - // Set up composite, font and rendering hints - cachedGraphics.setComposite(AlphaComposite.Src); - cachedGraphics.setColor(Color.WHITE); - cachedGraphics.setFont(font); - cachedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, - (antialiased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON - : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); - cachedGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, - (useFractionalMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON - : RenderingHints.VALUE_FRACTIONALMETRICS_OFF)); - } - return cachedGraphics; - } - private void beginRendering(boolean ortho, int width, int height, boolean disableDepthTestForOrtho) { - if (DEBUG && !debugged) { - debug(); + /** Draws the supplied CharSequence at the desired location using + the renderer's current color. The baseline of the leftmost + character is at position (x, y) specified in OpenGL coordinates, + where the origin is at the lower-left of the drawable and the Y + coordinate increases in the upward direction. + + @param str the string to draw + @param x the x coordinate at which to draw + @param y the y coordinate at which to draw + @throws GLException If an OpenGL context is not current when this method is called + */ + public void draw(CharSequence str, int x, int y) throws GLException { + draw3D(str, x, y, 0, 1); } - inBeginEndPair = true; - isOrthoMode = ortho; - beginRenderingWidth = width; - beginRenderingHeight = height; - beginRenderingDepthTestDisabled = disableDepthTestForOrtho; - if (ortho) { - getBackingStore().beginOrthoRendering(width, height, disableDepthTestForOrtho); - } else { - getBackingStore().begin3DRendering(); - } - GL gl = GLU.getCurrentGL(); - - // Push client attrib bits used by the pipelined quad renderer - gl.glPushClientAttrib((int) GL.GL_ALL_CLIENT_ATTRIB_BITS); - - if (!haveMaxSize) { - // Query OpenGL for the maximum texture size and set it in the - // RectanglePacker to keep it from expanding too large - int[] sz = new int[1]; - gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, sz, 0); - packer.setMaxSize(sz[0], sz[0]); - haveMaxSize = true; + /** Draws the supplied String at the desired location using the + renderer's current color. See {@link #draw(CharSequence, int, + int) draw(CharSequence, int, int)}. */ + public void draw(String str, int x, int y) throws GLException { + draw3D(str, x, y, 0, 1); } - if (needToResetColor && haveCachedColor) { - if (cachedColor == null) { - getBackingStore().setColor(cachedR, cachedG, cachedB, cachedA); - } else { - getBackingStore().setColor(cachedColor); - } - needToResetColor = false; + /** Draws the supplied CharSequence at the desired 3D location using + the renderer's current color. The baseline of the leftmost + character is placed at position (x, y, z) in the current + coordinate system. + + @param str the string to draw + @param x the x coordinate at which to draw + @param y the y coordinate at which to draw + @param z the z coordinate at which to draw + @param scaleFactor a uniform scale factor applied to the width and height of the drawn rectangle + @throws GLException If an OpenGL context is not current when this method is called + */ + public void draw3D(CharSequence str, float x, float y, float z, + float scaleFactor) { + internal_draw3D(str, x, y, z, scaleFactor); } - // Disable future attempts to use mipmapping if TextureRenderer - // doesn't support it - if (mipmap && !getBackingStore().isUsingAutoMipmapGeneration()) { - if (DEBUG) { - System.err.println("Disabled mipmapping in TextRenderer"); - } - mipmap = false; - } - } - - private void endRendering(boolean ortho) throws GLException { - flushGlyphPipeline (); - - inBeginEndPair = false; - - GL gl = GLU.getCurrentGL(); - // Pop client attrib bits used by the pipelined quad renderer - gl.glPopClientAttrib(); - // The OpenGL spec is unclear about whether this changes the - // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER - // binding - if (gl.isExtensionAvailable("GL_VERSION_1_5")) { - gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); + /** Draws the supplied String at the desired 3D location using the + renderer's current color. See {@link #draw3D(CharSequence, + float, float, float, float) draw3D(CharSequence, float, float, + float, float)}. */ + public void draw3D(String str, float x, float y, float z, float scaleFactor) { + internal_draw3D(str, x, y, z, scaleFactor); } - if (ortho) { - getBackingStore().endOrthoRendering(); - } else { - getBackingStore().end3DRendering(); + + /** Returns the pixel width of the given character. */ + public float getCharWidth(char inChar) { + return mGlyphProducer.getGlyphPixelWidth(inChar); } - if (++numRenderCycles >= CYCLES_PER_FLUSH) { - numRenderCycles = 0; - if (DEBUG) { - System.err.println("Clearing unused entries in endRendering()"); - } - clearUnusedEntries(); + + /** Causes the TextRenderer to flush any internal caches it may be + maintaining and draw its rendering results to the screen. This + should be called after each call to draw() if you are setting + OpenGL state such as the modelview matrix between calls to + draw(). */ + public void flush() { + flushGlyphPipeline(); } - } - - 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()); - } + + /** Ends a render cycle with this {@link TextRenderer TextRenderer}. + Restores the projection and modelview matrices as well as + several OpenGL state bits. Should be paired with {@link + #beginRendering beginRendering}. + + @throws GLException If an OpenGL context is not current when this method is called + */ + public void endRendering() throws GLException { + endRendering(true); } - } - - private void clearUnusedEntries() { - final java.util.List/*<Rect>*/ deadRects = new ArrayList/*<Rect>*/(); - // Iterate through the contents of the backing store, removing - // text strings that haven't been used recently - packer.visit(new RectVisitor() { - public void visit(Rect rect) { - TextData data = (TextData) rect.getUserData(); - if (data.used()) { - data.clearUsed(); - } else { - deadRects.add(rect); - } - } - }); - for (Iterator iter = deadRects.iterator(); iter.hasNext(); ) { - Rect r = (Rect) iter.next(); - packer.remove(r); - stringLocations.remove(((TextData) r.getUserData()).string()); - int unicodeToClearFromCache = ((TextData) r.getUserData()).unicodeID; - if (unicodeToClearFromCache > 0) - { - mGlyphProducer.clearCacheEntry(unicodeToClearFromCache); - } - - // if (DEBUG) { - // Graphics2D g = getGraphics2D(); - // g.setComposite(AlphaComposite.Clear); - // g.fillRect(r.x(), r.y(), r.w(), r.h()); - // g.setComposite(AlphaComposite.Src); - // } + + /** 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; } - // If we removed dead rectangles this cycle, try to do a compaction - float frag = packer.verticalFragmentationRatio(); - if (!deadRects.isEmpty() && frag > MAX_VERTICAL_FRAGMENTATION) { - if (DEBUG) { - System.err.println("Compacting TextRenderer backing store due to vertical fragmentation " + frag); - } - packer.compact(); + /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}. + Restores several OpenGL state bits. Should be paired with {@link + #begin3DRendering begin3DRendering}. + + @throws GLException If an OpenGL context is not current when this method is called + */ + public void end3DRendering() throws GLException { + endRendering(false); } - if (DEBUG) { - getBackingStore().markDirty(0, 0, getBackingStore().getWidth(), getBackingStore().getHeight()); + /** Disposes of all resources this TextRenderer is using. It is not + valid to use the TextRenderer after this method is called. + + @throws GLException If an OpenGL context is not current when this method is called + */ + public void dispose() throws GLException { + packer.dispose(); + packer = null; + cachedBackingStore = null; + cachedGraphics = null; + cachedFontRenderContext = null; + + if (dbgFrame != null) { + dbgFrame.dispose(); + } } - } - - class Manager implements BackingStoreManager - { - private Graphics2D g; - - public Object allocateBackingStore(int w, int h) { - // FIXME: should consider checking Font's attributes to see - // whether we're likely to need to support a full RGBA backing - // store (i.e., non-default Paint, foreground color, etc.), but - // for now, let's just be more efficient - TextureRenderer renderer; - - if (renderDelegate.intensityOnly()) { - renderer = TextureRenderer.createAlphaOnlyRenderer(w, h, mipmap); - } else { - renderer = new TextureRenderer(w, h, true, mipmap); - } - if (DEBUG) { - System.err.println(" TextRenderer allocating backing store " + w + " x " + h); - } - return renderer; + + //---------------------------------------------------------------------- + // 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); } - public void deleteBackingStore(Object backingStore) { - ((TextureRenderer) backingStore).dispose(); + private TextureRenderer getBackingStore() { + TextureRenderer renderer = (TextureRenderer) packer.getBackingStore(); + + if (renderer != cachedBackingStore) { + // Backing store changed since last time; discard any cached Graphics2D + if (cachedGraphics != null) { + cachedGraphics.dispose(); + cachedGraphics = null; + cachedFontRenderContext = null; + } + + cachedBackingStore = renderer; + } + + return cachedBackingStore; } - public boolean preExpand(Rect cause, int attemptNumber) { - // Only try this one time; clear out potentially obsolete entries - - // NOTE: this heuristic and the fact that it clears the used bit - // of all entries seems to cause cycling of entries in some - // situations, where the backing store becomes small compared to - // the amount of text on the screen (see the TextFlow demo) and - // the entries continually cycle in and out of the backing - // store, decreasing performance. If we added a little age - // information to the entries, and only cleared out entries - // above a certain age, this behavior would be eliminated. - // However, it seems the system usually stabilizes itself, so - // for now we'll just keep things simple. Note that if we don't - // clear the used bit here, the backing store tends to increase - // very quickly to its maximum size, at least with the TextFlow - // demo when the text is being continually re-laid out. - if (attemptNumber == 0) { - if (DEBUG) { - System.err.println("Clearing unused entries in preExpand(): attempt number " + attemptNumber); + private Graphics2D getGraphics2D() { + TextureRenderer renderer = getBackingStore(); + + if (cachedGraphics == null) { + cachedGraphics = renderer.createGraphics(); + + // Set up composite, font and rendering hints + cachedGraphics.setComposite(AlphaComposite.Src); + cachedGraphics.setColor(Color.WHITE); + cachedGraphics.setFont(font); + cachedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + (antialiased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON + : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); + cachedGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + (useFractionalMetrics + ? RenderingHints.VALUE_FRACTIONALMETRICS_ON + : RenderingHints.VALUE_FRACTIONALMETRICS_OFF)); } - clearUnusedEntries(); - return true; - } - return false; + return cachedGraphics; } - public void additionFailed(Rect cause, int attemptNumber) { - // Heavy hammer -- might consider doing something different - packer.clear(); - stringLocations.clear(); + private void beginRendering(boolean ortho, int width, int height, + boolean disableDepthTestForOrtho) { + if (DEBUG && !debugged) { + debug(); + } + + inBeginEndPair = true; + isOrthoMode = ortho; + beginRenderingWidth = width; + beginRenderingHeight = height; + beginRenderingDepthTestDisabled = disableDepthTestForOrtho; - if (DEBUG) { - System.err.println(" *** Cleared all text because addition failed ***"); - } + if (ortho) { + getBackingStore().beginOrthoRendering(width, height, + disableDepthTestForOrtho); + } else { + getBackingStore().begin3DRendering(); + } + + GL gl = GLU.getCurrentGL(); + + // Push client attrib bits used by the pipelined quad renderer + gl.glPushClientAttrib((int) GL.GL_ALL_CLIENT_ATTRIB_BITS); + + if (!haveMaxSize) { + // Query OpenGL for the maximum texture size and set it in the + // RectanglePacker to keep it from expanding too large + int[] sz = new int[1]; + gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, sz, 0); + packer.setMaxSize(sz[0], sz[0]); + haveMaxSize = true; + } + + if (needToResetColor && haveCachedColor) { + if (cachedColor == null) { + getBackingStore().setColor(cachedR, cachedG, cachedB, cachedA); + } else { + getBackingStore().setColor(cachedColor); + } + + needToResetColor = false; + } + + // Disable future attempts to use mipmapping if TextureRenderer + // doesn't support it + if (mipmap && !getBackingStore().isUsingAutoMipmapGeneration()) { + if (DEBUG) { + System.err.println("Disabled mipmapping in TextRenderer"); + } + + mipmap = false; + } } - public void beginMovement(Object oldBackingStore, Object newBackingStore) { - // Exit the begin / end pair if necessary - if (inBeginEndPair) { + private void endRendering(boolean ortho) throws GLException { + flushGlyphPipeline(); + + inBeginEndPair = false; + GL gl = GLU.getCurrentGL(); + // Pop client attrib bits used by the pipelined quad renderer gl.glPopClientAttrib(); + // The OpenGL spec is unclear about whether this changes the // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER // binding if (gl.isExtensionAvailable("GL_VERSION_1_5")) { - gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); } - if (isOrthoMode) { - ((TextureRenderer) oldBackingStore).endOrthoRendering(); + + if (ortho) { + getBackingStore().endOrthoRendering(); } else { - ((TextureRenderer) oldBackingStore).end3DRendering(); + getBackingStore().end3DRendering(); } - } - TextureRenderer newRenderer = (TextureRenderer) newBackingStore; - g = newRenderer.createGraphics(); - } - public void move(Object oldBackingStore, - Rect oldLocation, - Object newBackingStore, - Rect newLocation) { - TextureRenderer oldRenderer = (TextureRenderer) oldBackingStore; - TextureRenderer newRenderer = (TextureRenderer) newBackingStore; - - if (oldRenderer == newRenderer) { - // Movement on the same backing store -- easy case - g.copyArea(oldLocation.x(), oldLocation.y(), - oldLocation.w(), oldLocation.h(), - newLocation.x() - oldLocation.x(), - newLocation.y() - oldLocation.y()); - } else { - // Need to draw from the old renderer's image into the new one - Image img = oldRenderer.getImage(); - g.drawImage(img, - newLocation.x(), newLocation.y(), - newLocation.x() + newLocation.w(), newLocation.y() + newLocation.h(), - oldLocation.x(), oldLocation.y(), - oldLocation.x() + oldLocation.w(), oldLocation.y() + oldLocation.h(), - null); - } + if (++numRenderCycles >= CYCLES_PER_FLUSH) { + numRenderCycles = 0; + + if (DEBUG) { + System.err.println("Clearing unused entries in endRendering()"); + } + + clearUnusedEntries(); + } } - public void endMovement(Object oldBackingStore, Object newBackingStore) { - g.dispose(); - // Sync the whole surface - TextureRenderer newRenderer = (TextureRenderer) newBackingStore; - newRenderer.markDirty(0, 0, newRenderer.getWidth(), newRenderer.getHeight()); - // Re-enter the begin / end pair if necessary - if (inBeginEndPair) { - if (isOrthoMode) { - ((TextureRenderer) newBackingStore).beginOrthoRendering(beginRenderingWidth, - beginRenderingHeight, - beginRenderingDepthTestDisabled); + private void tokenize(CharSequence str) { + // Avoid lots of little allocations per render + tokenizationResults.clear(); + + if (!splitAtSpaces) { + tokenizationResults.add(str.toString()); } else { - ((TextureRenderer) newBackingStore).begin3DRendering(); + 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()); + } } - // Push client attrib bits used by the pipelined quad renderer - GL gl = GLU.getCurrentGL(); - gl.glPushClientAttrib((int) GL.GL_ALL_CLIENT_ATTRIB_BITS); - if (haveCachedColor) { - if (cachedColor == null) { - ((TextureRenderer) newBackingStore).setColor(cachedR, cachedG, cachedB, cachedA); - } else { - ((TextureRenderer) newBackingStore).setColor(cachedColor); - } - } - } else { - needToResetColor = true; - } } - } - public static class DefaultRenderDelegate implements RenderDelegate { - public boolean intensityOnly() { - return true; - } + private void clearUnusedEntries() { + final java.util.List deadRects = new ArrayList /*<Rect>*/(); + + // Iterate through the contents of the backing store, removing + // text strings that haven't been used recently + packer.visit(new RectVisitor() { + public void visit(Rect rect) { + TextData data = (TextData) rect.getUserData(); + + if (data.used()) { + data.clearUsed(); + } else { + deadRects.add(rect); + } + } + }); + + for (Iterator iter = deadRects.iterator(); iter.hasNext();) { + Rect r = (Rect) iter.next(); + packer.remove(r); + stringLocations.remove(((TextData) r.getUserData()).string()); + + int unicodeToClearFromCache = ((TextData) r.getUserData()).unicodeID; + + if (unicodeToClearFromCache > 0) { + mGlyphProducer.clearCacheEntry(unicodeToClearFromCache); + } + + // if (DEBUG) { + // Graphics2D g = getGraphics2D(); + // g.setComposite(AlphaComposite.Clear); + // g.fillRect(r.x(), r.y(), r.w(), r.h()); + // g.setComposite(AlphaComposite.Src); + // } + } + + // If we removed dead rectangles this cycle, try to do a compaction + float frag = packer.verticalFragmentationRatio(); + + if (!deadRects.isEmpty() && (frag > MAX_VERTICAL_FRAGMENTATION)) { + if (DEBUG) { + System.err.println( + "Compacting TextRenderer backing store due to vertical fragmentation " + + frag); + } + + packer.compact(); + } - public Rectangle2D getBounds(CharSequence str, - Font font, - FontRenderContext frc) { - GlyphVector gv = font.createGlyphVector(frc, new MapCharSequenceToGlyphVector(str)); - return gv.getPixelBounds(frc, 0, 0); + if (DEBUG) { + getBackingStore().markDirty(0, 0, getBackingStore().getWidth(), + getBackingStore().getHeight()); + } } - public Rectangle2D getBounds(String str, - Font font, - FontRenderContext frc) { - GlyphVector gv = font.createGlyphVector(frc, str); - return gv.getPixelBounds(frc, 0, 0); + //---------------------------------------------------------------------- + // Glyph-by-glyph rendering support + // + private void internal_draw3D(CharSequence str, float x, float y, float z, + float scaleFactor) { + GlyphsList glyphs = mGlyphProducer.getGlyphs(str); + + if (glyphs != null) { + drawGlyphs(glyphs, x, y, z, scaleFactor); + } else { + this.draw3D_ROBUST(str, x, y, z, scaleFactor); + } } - public void drawGlyphVector(Graphics2D graphics, GlyphVector str, int x, int y) { - graphics.drawGlyphVector(str, x, y); + private void flushGlyphPipeline() { + if (mPipelinedQuadRenderer != null) { + mPipelinedQuadRenderer.draw(); + } } - public void draw(Graphics2D graphics, String str, int x, int y) { - graphics.drawString(str, x, y); + private void drawGlyphs(GlyphsList inGlyphs, float inX, float inY, float z, + float scaleFactor) { + try { + if (mPipelinedQuadRenderer == null) { + mPipelinedQuadRenderer = new Pipelined_QuadRenderer(); + } + + TextureRenderer renderer = getBackingStore(); + float xOffset = 0; + + 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 = (float) texturex / (float) kSize; + float ty1 = 1.0f - ((float) texturey / (float) kSize); + float tx2 = (float) (texturex + width) / (float) kSize; + float ty2 = 1.0f - + ((float) (texturey + height) / (float) kSize); + + 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(); + } } - } - //---------------------------------------------------------------------- - // Glyph-by-glyph rendering support - // + private void drawGlyphsSIMPLE(GlyphsList inGlyphs, float x, float y, + float z, float scaleFactor) { // unused, for reference, debugging - private void internal_draw3D(CharSequence str,float x, float y, float z,float scaleFactor) - { - GlyphsList glyphs = mGlyphProducer.getGlyphs ( str); + TextureRenderer renderer = getBackingStore(); + int xOffset = 0; - if (glyphs != null) { - drawGlyphs(glyphs,x,y,z,scaleFactor); - } else { - this.draw3D_ROBUST ( str,x,y,z,scaleFactor); - } - } - - static final int kQuadsPerBuffer = 100; - static final int kCoordsPerVertVerts = 3; - static final int kCoordsPerVertTex = 2; - static final int kVertsPerQuad = 4; - - static final int kTotalBufferSizeVerts = kQuadsPerBuffer*kVertsPerQuad; - - static final int kTotalBufferSizeCoordsVerts = kQuadsPerBuffer*kVertsPerQuad*kCoordsPerVertVerts; - static final int kTotalBufferSizeCoordsTex = kQuadsPerBuffer*kVertsPerQuad*kCoordsPerVertTex; - static final int kTotalBufferSizeBytesVerts = kTotalBufferSizeCoordsVerts*4; - static final int kTotalBufferSizeBytesTex = kTotalBufferSizeCoordsTex*4; - - static final int kSizeInBytes_OneVertices_VertexData = kCoordsPerVertVerts*4; - static final int kSizeInBytes_OneVertices_TexData = kCoordsPerVertTex*4; - - 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++; - } + GL gl = GLU.getCurrentGL(); - 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()); + 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. } - else - { - outList.textureSourceRect[this.renderIndex[i]] = mapper.glyphRectForTextureMapping[this.newGlyphs[i]]; + } + } + + 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()); + } + + // 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(); + + // 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); } + xOffset += (getSpaceWidth() * scaleFactor); } } - public void allocateSpace ( int inLength ) - { - int allocLength = Math.max(inLength,100); + //---------------------------------------------------------------------- + // Debugging functionality + // + private void debug() { + dbgFrame = new Frame("TextRenderer Debug Output"); + + GLCanvas dbgCanvas = new GLCanvas(new GLCapabilities(), null, + GLContext.getCurrent(), null); + dbgCanvas.addGLEventListener(new DebugListener(dbgFrame)); + dbgFrame.add(dbgCanvas); + + final FPSAnimator anim = new FPSAnimator(dbgCanvas, 10); + dbgFrame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + // Run this on another thread than the AWT event queue to + // make sure the call to Animator.stop() completes before + // exiting + new Thread(new Runnable() { + public void run() { + anim.stop(); + } + }).start(); + } + }); + dbgFrame.setSize(kSize, kSize); + dbgFrame.setVisible(true); + anim.start(); + debugged = true; + } - 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 supporting more full control over the process of rendering + the bitmapped text. Allows customization of whether the backing + store text bitmap is full-color or intensity only, the size of + each individual rendered text rectangle, and the contents of + each individual rendered text string. The default implementation + of this interface uses an intensity-only texture, a + closely-cropped rectangle around the text, and renders text + using the color white, which is modulated by the set color + during the rendering process. */ + public static interface RenderDelegate { + /** Indicates whether the backing store of this TextRenderer + should be intensity-only (the default) or full-color. */ + public boolean intensityOnly(); + + /** Computes the bounds of the given String relative to the + origin. */ + public Rectangle2D getBounds(String str, Font font, + FontRenderContext frc); + + /** Computes the bounds of the given character sequence relative + to the origin. */ + public Rectangle2D getBounds(CharSequence str, Font font, + FontRenderContext frc); + + /** Render the passed character sequence at the designated + location using the supplied Graphics2D instance. The + surrounding region will already have been cleared to the RGB + color (0, 0, 0) with zero alpha. The initial drawing context + of the passed Graphics2D will be set to use + AlphaComposite.Src, the color white, the Font specified in the + TextRenderer's constructor, and the rendering hints specified + in the TextRenderer constructor. Changes made by the end user + may be visible in successive calls to this method, but are not + guaranteed to be preserved. Implementors of this method + should reset the Graphics2D's state to that desired each time + this method is called, in particular those states which are + not the defaults. */ + public void draw(Graphics2D graphics, String str, int x, int y); + + /** Render the passed GlyphVector at the designated location using + the supplied Graphics2D instance. The surrounding region will + already have been cleared to the RGB color (0, 0, 0) with zero + alpha. The initial drawing context of the passed Graphics2D + will be set to use AlphaComposite.Src, the color white, the + Font specified in the TextRenderer's constructor, and the + rendering hints specified in the TextRenderer constructor. + Changes made by the end user may be visible in successive + calls to this method, but are not guaranteed to be preserved. + Implementors of this method should reset the Graphics2D's + state to that desired each time this method is called, in + particular those states which are not the defaults. */ + public void drawGlyphVector(Graphics2D graphics, GlyphVector str, + int x, int y); } - } - static class GlyphsList - { - float[] advances; - Rect[] textureSourceRect; - int length; + private static class MapCharSequenceToGlyphVector + implements CharacterIterator { + CharSequence mSequence; + int mLength; + int mCurrentIndex; - public void allocateSpace ( int inLength ) - { - int allocLength = Math.max(inLength,100); + MapCharSequenceToGlyphVector() { + } - if (advances == null || advances.length < allocLength) - { - advances = new float[allocLength]; - textureSourceRect = new Rect[allocLength]; + MapCharSequenceToGlyphVector(CharSequence sequence) { + initFromCharSequence(sequence); } - } - } - - 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; - - 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]; - - for (int i=0;i<unicodes2Glyphs.length;i++) - { - unicodes2Glyphs[i] = undefined; + + public void initFromCharSequence(CharSequence sequence) { + mSequence = sequence; + mLength = mSequence.length(); + mCurrentIndex = 0; + } + + public char last() { + mCurrentIndex = Math.max(0, mLength - 1); + + return current(); + } + + public char current() { + if ((mLength == 0) || (mCurrentIndex >= mLength)) { + return CharacterIterator.DONE; } + + return mSequence.charAt(mCurrentIndex); } - } - public void clearCacheEntry(int unicodeID) - { - unicodes2Glyphs[unicodeID] = undefined; - } + public char next() { + mCurrentIndex++; - public void allocateSpace(int length) - { - length = Math.max(length, 100); + return current(); + } + + public char previous() { + mCurrentIndex = Math.max(mCurrentIndex - 1, 0); - if (unicodes == null || unicodes.length < length) - { - unicodes = new char[length]; + return current(); } - glyphsToUpload.allocateSpace(length); - glyphsOutput.allocateSpace(length); - } + public char setIndex(int position) { + mCurrentIndex = position; - MapCharSequenceToGlyphVector iter = new MapCharSequenceToGlyphVector(); + return current(); + } - char [] tempChars = new char[1]; - float getGlyphPixelWidth(char unicodeID) - { - int glyphID = undefined; + public int getBeginIndex() { + return 0; + } - 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 + public int getEndIndex() { + return mLength; } - if (glyphID != undefined) // if we haven't, we must get some its attributes, and prep for upload - { - return advances[glyphID]; + public int getIndex() { + return mCurrentIndex; } - else - { - tempChars[0] = unicodeID; - GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext,tempChars); - return fullRunGlyphVector.getGlyphMetrics(0).getAdvance (); + public Object clone() { + MapCharSequenceToGlyphVector iter = new MapCharSequenceToGlyphVector(mSequence); + iter.mCurrentIndex = mCurrentIndex; + + return iter; } - // return -1; + public char first() { + if (mLength == 0) { + return CharacterIterator.DONE; + } + + mCurrentIndex = 0; + + return current(); + } } - GlyphsList getGlyphs(CharSequence inString) - { - glyphsToUpload.numberOfNewGlyphs = 0; + // Data associated with each rectangle of text + static class TextData { + 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 + // 2D coordinate system) at which the string must be rasterized in + // order to fit within the rectangle -- the leftmost point of the + // baseline. + private Point origin; + private boolean used; // Whether this text was used recently + + TextData(String str, Point origin, int unicodeID) { + this.str = str; + this.origin = origin; + this.unicodeID = unicodeID; + } - int length = inString.length (); - allocateSpace(length); + String string() { + return str; + } - iter.initFromCharSequence ( inString); - - GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext,iter); - boolean complex = (fullRunGlyphVector.getLayoutFlags () != 0); - int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs (); + Point origin() { + return origin; + } - if (complex) - { - return null; + boolean used() { + return used; } - for (int i=0;i<lengthInGlyphs;i++) - { - char unicodeID = inString.charAt (i); + void markUsed() { + used = true; + } + + void clearUsed() { + used = false; + } + } + + class Manager implements BackingStoreManager { + private Graphics2D g; + + public Object allocateBackingStore(int w, int h) { + // FIXME: should consider checking Font's attributes to see + // whether we're likely to need to support a full RGBA backing + // store (i.e., non-default Paint, foreground color, etc.), but + // for now, let's just be more efficient + TextureRenderer renderer; + + if (renderDelegate.intensityOnly()) { + renderer = TextureRenderer.createAlphaOnlyRenderer(w, h, mipmap); + } else { + renderer = new TextureRenderer(w, h, true, mipmap); + } - if (unicodeID >= unicodes2Glyphs.length) // <-- -could support these better - { - return null; + if (DEBUG) { + System.err.println(" TextRenderer allocating backing store " + + w + " x " + h); } - int glyphID = unicodes2Glyphs[unicodeID]; // Check to see if we have already encountered this unicode + return renderer; + } + + public void deleteBackingStore(Object backingStore) { + ((TextureRenderer) backingStore).dispose(); + } + + public boolean preExpand(Rect cause, int attemptNumber) { + // Only try this one time; clear out potentially obsolete entries + // NOTE: this heuristic and the fact that it clears the used bit + // of all entries seems to cause cycling of entries in some + // situations, where the backing store becomes small compared to + // the amount of text on the screen (see the TextFlow demo) and + // the entries continually cycle in and out of the backing + // store, decreasing performance. If we added a little age + // information to the entries, and only cleared out entries + // above a certain age, this behavior would be eliminated. + // However, it seems the system usually stabilizes itself, so + // for now we'll just keep things simple. Note that if we don't + // clear the used bit here, the backing store tends to increase + // very quickly to its maximum size, at least with the TextFlow + // demo when the text is being continually re-laid out. + if (attemptNumber == 0) { + if (DEBUG) { + System.err.println( + "Clearing unused entries in preExpand(): attempt number " + + attemptNumber); + } - 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; - GlyphVector gv = font.createGlyphVector(fontRenderContext, singleUnicode); // need this to get single bitmaps - glyphID = gv.getGlyphCode ( 0); + clearUnusedEntries(); + + return true; + } + + return false; + } - glyphsToUpload.prepGlyphForUpload ( unicodeID, gv.getGlyphCode ( 0), gv.getPixelBounds (fontRenderContext, 0, 0), i, gv ); + public void additionFailed(Rect cause, int attemptNumber) { + // Heavy hammer -- might consider doing something different + packer.clear(); + stringLocations.clear(); - advances[glyphID] = metrics.getAdvance (); + if (DEBUG) { + System.err.println( + " *** Cleared all text because addition failed ***"); } - else - { - glyphsOutput.textureSourceRect[i] = glyphRectForTextureMapping[glyphID]; + } + + public void beginMovement(Object oldBackingStore, Object newBackingStore) { + // Exit the begin / end pair if necessary + if (inBeginEndPair) { + GL gl = GLU.getCurrentGL(); + + // Pop client attrib bits used by the pipelined quad renderer + gl.glPopClientAttrib(); + + // The OpenGL spec is unclear about whether this changes the + // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER + // binding + if (gl.isExtensionAvailable("GL_VERSION_1_5")) { + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); + } + + if (isOrthoMode) { + ((TextureRenderer) oldBackingStore).endOrthoRendering(); + } else { + ((TextureRenderer) oldBackingStore).end3DRendering(); + } } - glyphsOutput.advances[i] = advances[glyphID]; + TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + g = newRenderer.createGraphics(); } - glyphsOutput.length = length; + public void move(Object oldBackingStore, Rect oldLocation, + Object newBackingStore, Rect newLocation) { + TextureRenderer oldRenderer = (TextureRenderer) oldBackingStore; + TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + + if (oldRenderer == newRenderer) { + // Movement on the same backing store -- easy case + g.copyArea(oldLocation.x(), oldLocation.y(), oldLocation.w(), + oldLocation.h(), newLocation.x() - oldLocation.x(), + newLocation.y() - oldLocation.y()); + } else { + // Need to draw from the old renderer's image into the new one + Image img = oldRenderer.getImage(); + g.drawImage(img, newLocation.x(), newLocation.y(), + newLocation.x() + newLocation.w(), + newLocation.y() + newLocation.h(), oldLocation.x(), + oldLocation.y(), oldLocation.x() + oldLocation.w(), + oldLocation.y() + oldLocation.h(), null); + } + } - glyphsToUpload.uploadAnyNewGlyphs( glyphsOutput,this); + public void endMovement(Object oldBackingStore, Object newBackingStore) { + g.dispose(); + + // Sync the whole surface + TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + newRenderer.markDirty(0, 0, newRenderer.getWidth(), + newRenderer.getHeight()); + + // Re-enter the begin / end pair if necessary + if (inBeginEndPair) { + if (isOrthoMode) { + ((TextureRenderer) newBackingStore).beginOrthoRendering(beginRenderingWidth, + beginRenderingHeight, beginRenderingDepthTestDisabled); + } else { + ((TextureRenderer) newBackingStore).begin3DRendering(); + } - return glyphsOutput; + // Push client attrib bits used by the pipelined quad renderer + GL gl = GLU.getCurrentGL(); + gl.glPushClientAttrib((int) GL.GL_ALL_CLIENT_ATTRIB_BITS); + + if (haveCachedColor) { + if (cachedColor == null) { + ((TextureRenderer) newBackingStore).setColor(cachedR, + cachedG, cachedB, cachedA); + } else { + ((TextureRenderer) newBackingStore).setColor(cachedColor); + } + } + } else { + needToResetColor = true; + } + } } - } - - class Pipelined_QuadRenderer - { - int mOutstandingGlyphsVerticesPipeline = 0; - FloatBuffer mTexCoords; - FloatBuffer mVertCoords; - boolean usingVBOs; - int mVBO_For_ResuableTileVertices; - int mVBO_For_ResuableTileTexCoords; + public static class DefaultRenderDelegate implements RenderDelegate { + public boolean intensityOnly() { + return true; + } - Pipelined_QuadRenderer () - { - GL gl = GLU.getCurrentGL(); - mVertCoords = BufferUtil.newFloatBuffer( kTotalBufferSizeCoordsVerts); - mTexCoords = BufferUtil.newFloatBuffer( kTotalBufferSizeCoordsTex); + public Rectangle2D getBounds(CharSequence str, Font font, + FontRenderContext frc) { + GlyphVector gv = font.createGlyphVector(frc, + new MapCharSequenceToGlyphVector(str)); - usingVBOs = (gl.isExtensionAvailable("GL_VERSION_1_5")); + return gv.getPixelBounds(frc, 0, 0); + } - if (usingVBOs) { - int[] vbos = new int[2]; - gl.glGenBuffers( 2, IntBuffer.wrap(vbos )); + public Rectangle2D getBounds(String str, Font font, + FontRenderContext frc) { + GlyphVector gv = font.createGlyphVector(frc, str); - mVBO_For_ResuableTileVertices = vbos[0]; - mVBO_For_ResuableTileTexCoords = vbos[1]; + return gv.getPixelBounds(frc, 0, 0); + } - gl.glBindBuffer( GL.GL_ARRAY_BUFFER, mVBO_For_ResuableTileVertices); - gl.glBufferData( GL.GL_ARRAY_BUFFER, kTotalBufferSizeBytesVerts,null, GL.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline + public void drawGlyphVector(Graphics2D graphics, GlyphVector str, + int x, int y) { + graphics.drawGlyphVector(str, x, y); + } - gl.glBindBuffer( GL.GL_ARRAY_BUFFER, mVBO_For_ResuableTileTexCoords); - gl.glBufferData( GL.GL_ARRAY_BUFFER, kTotalBufferSizeBytesTex,null, GL.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline - } + public void draw(Graphics2D graphics, String str, int x, int y) { + graphics.drawString(str, x, y); + } } - public void glTexCoord2f ( float v, float v1 ) - { - mTexCoords.put(v); - mTexCoords.put(v1); + 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]]; + } + } + } + + public void allocateSpace(int inLength) { + int allocLength = Math.max(inLength, 100); + + 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]; + } + } } - public void glVertex3f ( float inX, float inY, float inZ ) - { - mVertCoords.put(inX); - mVertCoords.put(inY); - mVertCoords.put(inZ); + static class GlyphsList { + float[] advances; + Rect[] textureSourceRect; + int length; - mOutstandingGlyphsVerticesPipeline++; + public void allocateSpace(int inLength) { + int allocLength = Math.max(inLength, 100); - if ( mOutstandingGlyphsVerticesPipeline >= kTotalBufferSizeVerts) - { - this.draw(); + if ((advances == null) || (advances.length < allocLength)) { + advances = new float[allocLength]; + textureSourceRect = new Rect[allocLength]; + } } } - private void draw() - { - if ( mOutstandingGlyphsVerticesPipeline > 0) - { - GL gl = GLU.getCurrentGL(); + 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]; + + for (int i = 0; i < unicodes2Glyphs.length; i++) { + unicodes2Glyphs[i] = undefined; + } + } + } + + public void clearCacheEntry(int unicodeID) { + unicodes2Glyphs[unicodeID] = undefined; + } + + public void allocateSpace(int length) { + length = Math.max(length, 100); - TextureRenderer renderer = getBackingStore (); - Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious? + if ((unicodes == null) || (unicodes.length < length)) { + unicodes = new char[length]; + } - mVertCoords.rewind(); - mTexCoords.rewind(); + glyphsToUpload.allocateSpace(length); + glyphsOutput.allocateSpace(length); + } - gl.glEnableClientState(GL.GL_VERTEX_ARRAY); - if (usingVBOs) { - gl.glBindBuffer( GL.GL_ARRAY_BUFFER, mVBO_For_ResuableTileVertices); - gl.glBufferSubData( GL.GL_ARRAY_BUFFER, 0, mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_VertexData, mVertCoords ); // upload only the new stuff - gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0); - } else { - gl.glVertexPointer(3, GL.GL_FLOAT, 0, mVertCoords); - } + float getGlyphPixelWidth(char unicodeID) { + int glyphID = undefined; - gl.glClientActiveTexture(GL.GL_TEXTURE0); - gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY); + 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 + } - if (usingVBOs) { - gl.glBindBuffer( GL.GL_ARRAY_BUFFER, mVBO_For_ResuableTileTexCoords); - gl.glBufferSubData( GL.GL_ARRAY_BUFFER,0, mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_TexData, mTexCoords ); // upload only the new stuff - gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0); - } else { - gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, mTexCoords); - } + if (glyphID != undefined) { // if we haven't, we must get some its attributes, and prep for upload - gl.glDrawArrays(GL.GL_QUADS,0, mOutstandingGlyphsVerticesPipeline ); + return advances[glyphID]; + } else { + tempChars[0] = unicodeID; - mVertCoords.rewind(); - mTexCoords.rewind(); - mOutstandingGlyphsVerticesPipeline = 0; + GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext, + tempChars); + + return fullRunGlyphVector.getGlyphMetrics(0).getAdvance(); + } + + // return -1; } - } - private void drawIMMEDIATE() - { - if ( mOutstandingGlyphsVerticesPipeline > 0) - { - TextureRenderer renderer = getBackingStore(); - Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious? + GlyphsList getGlyphs(CharSequence inString) { + glyphsToUpload.numberOfNewGlyphs = 0; - GL gl = GLU.getCurrentGL(); - gl.glBegin ( GL.GL_QUADS); + int length = inString.length(); + allocateSpace(length); - try - { - int numberOfQuads = mOutstandingGlyphsVerticesPipeline /4; - mVertCoords.rewind(); - mTexCoords.rewind(); + iter.initFromCharSequence(inString); - for (int i=0;i<numberOfQuads;i++) - { - gl.glTexCoord2f( mTexCoords.get (), mTexCoords.get ()); - gl.glVertex3f( mVertCoords.get (), mVertCoords.get (), mVertCoords.get()); + GlyphVector fullRunGlyphVector = font.createGlyphVector(fontRenderContext, + iter); + boolean complex = (fullRunGlyphVector.getLayoutFlags() != 0); + int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs(); + + if (complex) { + return null; + } - gl.glTexCoord2f( mTexCoords.get (), mTexCoords.get ()); - gl.glVertex3f( mVertCoords.get (), mVertCoords.get (), mVertCoords.get()); + for (int i = 0; i < lengthInGlyphs; i++) { + char unicodeID = inString.charAt(i); - gl.glTexCoord2f( mTexCoords.get (), mTexCoords.get ()); - gl.glVertex3f( mVertCoords.get (), mVertCoords.get (), mVertCoords.get()); + if (unicodeID >= unicodes2Glyphs.length) { // <-- -could support these better - gl.glTexCoord2f( mTexCoords.get (), mTexCoords.get ()); - gl.glVertex3f( mVertCoords.get (), mVertCoords.get (), mVertCoords.get()); + return null; } + + int glyphID = unicodes2Glyphs[unicodeID]; // Check to see if we have already encountered this unicode + + 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; + + GlyphVector gv = font.createGlyphVector(fontRenderContext, + singleUnicode); // need this to get single bitmaps + glyphID = gv.getGlyphCode(0); + + glyphsToUpload.prepGlyphForUpload(unicodeID, + gv.getGlyphCode(0), + gv.getPixelBounds(fontRenderContext, 0, 0), i, gv); + + advances[glyphID] = metrics.getAdvance(); + } else { + glyphsOutput.textureSourceRect[i] = glyphRectForTextureMapping[glyphID]; + } + + glyphsOutput.advances[i] = advances[glyphID]; + } + + glyphsOutput.length = length; + + glyphsToUpload.uploadAnyNewGlyphs(glyphsOutput, this); + + return glyphsOutput; + } + } + + class Pipelined_QuadRenderer { + int mOutstandingGlyphsVerticesPipeline = 0; + FloatBuffer mTexCoords; + FloatBuffer mVertCoords; + boolean usingVBOs; + int mVBO_For_ResuableTileVertices; + int mVBO_For_ResuableTileTexCoords; + + Pipelined_QuadRenderer() { + GL gl = GLU.getCurrentGL(); + mVertCoords = BufferUtil.newFloatBuffer(kTotalBufferSizeCoordsVerts); + mTexCoords = BufferUtil.newFloatBuffer(kTotalBufferSizeCoordsTex); + + usingVBOs = (gl.isExtensionAvailable("GL_VERSION_1_5")); + + if (usingVBOs) { + int[] vbos = new int[2]; + gl.glGenBuffers(2, IntBuffer.wrap(vbos)); + + mVBO_For_ResuableTileVertices = vbos[0]; + mVBO_For_ResuableTileTexCoords = vbos[1]; + + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, + mVBO_For_ResuableTileVertices); + gl.glBufferData(GL.GL_ARRAY_BUFFER, kTotalBufferSizeBytesVerts, + null, GL.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline + + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, + mVBO_For_ResuableTileTexCoords); + gl.glBufferData(GL.GL_ARRAY_BUFFER, kTotalBufferSizeBytesTex, + null, GL.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline + } + } + + public void glTexCoord2f(float v, float v1) { + mTexCoords.put(v); + mTexCoords.put(v1); + } + + public void glVertex3f(float inX, float inY, float inZ) { + mVertCoords.put(inX); + mVertCoords.put(inY); + mVertCoords.put(inZ); + + mOutstandingGlyphsVerticesPipeline++; + + if (mOutstandingGlyphsVerticesPipeline >= kTotalBufferSizeVerts) { + this.draw(); } - catch(Exception e) - { - e.printStackTrace (); + } + + private void draw() { + if (mOutstandingGlyphsVerticesPipeline > 0) { + GL gl = GLU.getCurrentGL(); + + TextureRenderer renderer = getBackingStore(); + Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious? + + mVertCoords.rewind(); + mTexCoords.rewind(); + + gl.glEnableClientState(GL.GL_VERTEX_ARRAY); + + if (usingVBOs) { + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, + mVBO_For_ResuableTileVertices); + gl.glBufferSubData(GL.GL_ARRAY_BUFFER, 0, + mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_VertexData, + mVertCoords); // upload only the new stuff + gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0); + } else { + gl.glVertexPointer(3, GL.GL_FLOAT, 0, mVertCoords); + } + + gl.glClientActiveTexture(GL.GL_TEXTURE0); + gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY); + + if (usingVBOs) { + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, + mVBO_For_ResuableTileTexCoords); + gl.glBufferSubData(GL.GL_ARRAY_BUFFER, 0, + mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_TexData, + mTexCoords); // upload only the new stuff + gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0); + } else { + gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, mTexCoords); + } + + gl.glDrawArrays(GL.GL_QUADS, 0, + mOutstandingGlyphsVerticesPipeline); + + mVertCoords.rewind(); + mTexCoords.rewind(); + mOutstandingGlyphsVerticesPipeline = 0; } - finally - { - gl.glEnd(); - mVertCoords.rewind(); - mTexCoords.rewind(); - mOutstandingGlyphsVerticesPipeline = 0; + } + + private void drawIMMEDIATE() { + if (mOutstandingGlyphsVerticesPipeline > 0) { + TextureRenderer renderer = getBackingStore(); + Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious? + + GL gl = GLU.getCurrentGL(); + gl.glBegin(GL.GL_QUADS); + + try { + int numberOfQuads = mOutstandingGlyphsVerticesPipeline / 4; + mVertCoords.rewind(); + mTexCoords.rewind(); + + for (int i = 0; i < numberOfQuads; i++) { + gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get()); + gl.glVertex3f(mVertCoords.get(), mVertCoords.get(), + mVertCoords.get()); + + gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get()); + gl.glVertex3f(mVertCoords.get(), mVertCoords.get(), + mVertCoords.get()); + + gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get()); + gl.glVertex3f(mVertCoords.get(), mVertCoords.get(), + mVertCoords.get()); + + gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get()); + gl.glVertex3f(mVertCoords.get(), mVertCoords.get(), + mVertCoords.get()); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + gl.glEnd(); + mVertCoords.rewind(); + mTexCoords.rewind(); + mOutstandingGlyphsVerticesPipeline = 0; + } } } } - } - - Pipelined_QuadRenderer mPipelinedQuadRenderer; - - - private void flushGlyphPipeline () - { - if (mPipelinedQuadRenderer != null) mPipelinedQuadRenderer.draw (); - } - - private void drawGlyphs ( GlyphsList inGlyphs, float inX, float inY, float z,float scaleFactor ) - { - try - { - if (mPipelinedQuadRenderer == null) - { - mPipelinedQuadRenderer = new Pipelined_QuadRenderer (); - } - - TextureRenderer renderer = getBackingStore(); - float xOffset = 0; - - 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 = ( float ) texturex / ( float ) kSize; - float ty1 = 1.0f- (( float ) texturey / ( float ) kSize); - float tx2 = ( float ) (texturex + width) / ( float ) kSize; - float ty2 = 1.0f- (( float ) (texturey + height) / ( float ) kSize); - - 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 (); - } - } - - private void drawGlyphsSIMPLE ( GlyphsList inGlyphs, float x, float y, float z,float scaleFactor ) // unused, for reference, debugging - { - TextureRenderer renderer = getBackingStore(); - int xOffset = 0; - - GL gl = GLU.getCurrentGL(); - - - 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. - } - } - } - - 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()); - } - - // 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(); - - // 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; - } - xOffset += getSpaceWidth() * scaleFactor; - } - } - - //---------------------------------------------------------------------- - // Debugging functionality - // - - private void debug() { - dbgFrame = new Frame("TextRenderer Debug Output"); - GLCanvas dbgCanvas = new GLCanvas(new GLCapabilities (), null, GLContext.getCurrent(), null); - dbgCanvas.addGLEventListener(new DebugListener(dbgFrame)); - dbgFrame.add(dbgCanvas); - final FPSAnimator anim = new FPSAnimator(dbgCanvas, 10); - dbgFrame.addWindowListener(new WindowAdapter() { - public void windowClosing( WindowEvent e) { - // Run this on another thread than the AWT event queue to - // make sure the call to Animator.stop() completes before - // exiting - new Thread(new Runnable() { - public void run() { - anim.stop(); - } - }).start(); - } - }); - dbgFrame.setSize(kSize, kSize); - dbgFrame.setVisible(true); - anim.start(); - debugged = true; - } - - class DebugListener implements GLEventListener { - private GLU glu = new GLU(); - private Frame frame; - - DebugListener(Frame frame) { - this.frame = frame; - } + class DebugListener implements GLEventListener { + private GLU glu = new GLU(); + private Frame frame; - public void display(GLAutoDrawable drawable) { - GL gl = drawable.getGL(); - gl.glClear(GL.GL_DEPTH_BUFFER_BIT | GL.GL_COLOR_BUFFER_BIT); - if (packer == null) - return; - TextureRenderer rend = getBackingStore(); - final int w = rend.getWidth(); - final int h = rend.getHeight(); - rend.beginOrthoRendering(w, h); - rend.drawOrthoRect(0, 0); - rend.endOrthoRendering(); - if (frame.getWidth() != w || - frame.getHeight() != h) { - EventQueue.invokeLater(new Runnable() { - public void run() { - frame.setSize(w, h); + DebugListener(Frame frame) { + this.frame = frame; + } + + public void display(GLAutoDrawable drawable) { + GL gl = drawable.getGL(); + gl.glClear(GL.GL_DEPTH_BUFFER_BIT | GL.GL_COLOR_BUFFER_BIT); + + if (packer == null) { + return; } - }); - } - } - // Unused methods - public void init(GLAutoDrawable drawable) {} - public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {} - public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {} - } + TextureRenderer rend = getBackingStore(); + final int w = rend.getWidth(); + final int h = rend.getHeight(); + rend.beginOrthoRendering(w, h); + rend.drawOrthoRect(0, 0); + rend.endOrthoRendering(); + + if ((frame.getWidth() != w) || (frame.getHeight() != h)) { + EventQueue.invokeLater(new Runnable() { + public void run() { + frame.setSize(w, h); + } + }); + } + } + + // Unused methods + public void init(GLAutoDrawable drawable) { + } + + public void reshape(GLAutoDrawable drawable, int x, int y, int width, + int height) { + } + + public void displayChanged(GLAutoDrawable drawable, + boolean modeChanged, boolean deviceChanged) { + } + } } |