diff options
Diffstat (limited to 'src/classes')
5 files changed, 450 insertions, 84 deletions
diff --git a/src/classes/com/sun/opengl/impl/packrect/BackingStoreManager.java b/src/classes/com/sun/opengl/impl/packrect/BackingStoreManager.java index ba4b0cf30..0c4636063 100755 --- a/src/classes/com/sun/opengl/impl/packrect/BackingStoreManager.java +++ b/src/classes/com/sun/opengl/impl/packrect/BackingStoreManager.java @@ -49,16 +49,37 @@ public interface BackingStoreManager { public Object allocateBackingStore(int w, int h); public void deleteBackingStore(Object backingStore); - // Notification that movement is starting + /** Notification that expansion of the backing store is about to be + done due to addition of the given rectangle. Gives the manager a + chance to do some compaction and potentially remove old entries + from the backing store, if it acts like a least-recently-used + cache. This method receives as argument the number of attempts + so far to add the given rectangle. Manager should return true if + the RectanglePacker should retry the addition (which may result + in this method being called again, with an increased attempt + number) or false if the RectanglePacker should just expand the + backing store. The caller should not call RectanglePacker.add() + in its preExpand() method. */ + public boolean preExpand(Rect cause, int attemptNumber); + + /** Notification that addition of the given Rect failed because a + maximum size was set in the RectanglePacker and the backing + store could not be expanded. */ + public void additionFailed(Rect cause, int attemptNumber); + + /** Notification that movement is starting. */ public void beginMovement(Object oldBackingStore, Object newBackingStore); - // Can the backing stores be identical? I think so, in the case of - // compacting the existing backing store as opposed to reallocating it... + /** Tells the manager to move the contents of the given rect from + the old location on the old backing store to the new location on + the new backing store. The backing stores can be identical in + the case of compacting the existing backing store instead of + reallocating it. */ public void move(Object oldBackingStore, Rect oldLocation, Object newBackingStore, Rect newLocation); - // Notification that movement is ending + /** Notification that movement is ending. */ public void endMovement(Object oldBackingStore, Object newBackingStore); } diff --git a/src/classes/com/sun/opengl/impl/packrect/Level.java b/src/classes/com/sun/opengl/impl/packrect/Level.java index e7eab9ca7..12a09cd9a 100755 --- a/src/classes/com/sun/opengl/impl/packrect/Level.java +++ b/src/classes/com/sun/opengl/impl/packrect/Level.java @@ -129,8 +129,7 @@ public class Level { freeList.add(candidate); } - if (freeList.isEmpty()) - freeList = null; + coalesceFreeList(); return true; } @@ -148,38 +147,22 @@ public class Level { // to the free list, we can just decrease the nextAddX if (rect.maxX() + 1 == nextAddX) { nextAddX -= rect.w(); - - // Now try to coalesce additional free space at the end of the - // free list - if (freeList != null) { - boolean found = true; - while (found) { - found = false; - for (Iterator iter = freeList.iterator(); iter.hasNext(); ) { - Rect cur = (Rect) iter.next(); - if (cur.maxX() + 1 == nextAddX) { - nextAddX -= cur.w(); - found = true; - freeList.remove(cur); - break; - } - } - } - if (freeList.isEmpty()) - freeList = null; + } else { + if (freeList == null) { + freeList = new ArrayList/*<Rect>*/(); } - - return true; + freeList.add(new Rect(rect.x(), rect.y(), rect.w(), height, null)); + coalesceFreeList(); } - // Else, add the space consumed by this rectangle to the free list - if (freeList == null) { - freeList = new ArrayList/*<Rect>*/(); - } - freeList.add(new Rect(rect.x(), rect.y(), rect.w(), height, null)); return true; } + /** Indicates whether this Level contains no rectangles. */ + public boolean isEmpty() { + return rects.isEmpty(); + } + /** Indicates whether this Level could satisfy an allocation request if it were compacted. */ public boolean couldAllocateIfCompacted(Rect rect) { @@ -211,6 +194,7 @@ public class Level { nextCompactionDest += cur.w(); } nextAddX = nextCompactionDest; + freeList.clear(); manager.endMovement(backingStore, backingStore); } @@ -240,4 +224,52 @@ public class Level { rects.set(i, next); } } + + private void coalesceFreeList() { + if (freeList == null) + return; + if (freeList.isEmpty()) + return; + + // Try to coalesce adjacent free blocks in the free list + Collections.sort(freeList, rectXComparator); + int i = 0; + while (i < freeList.size() - 1) { + Rect r1 = (Rect) freeList.get(i); + Rect r2 = (Rect) freeList.get(i+1); + if (r1.maxX() + 1 == r2.x()) { + // Coalesce r1 and r2 into one block + freeList.remove(i+1); + r1.setSize(r1.w() + r2.w(), r1.h()); + } else { + ++i; + } + } + // See whether the last block bumps up against the addition point + Rect last = (Rect) freeList.get(freeList.size() - 1); + if (last.maxX() + 1 == nextAddX) { + nextAddX -= last.w(); + freeList.remove(freeList.size() - 1); + } + if (freeList.isEmpty()) { + freeList = null; + } + } + + //---------------------------------------------------------------------- + // Debugging functionality + // + + public void dumpFreeSpace() { + int freeListWidth = 0; + for (Iterator iter = freeList.iterator(); iter.hasNext(); ) { + Rect cur = (Rect) iter.next(); + System.err.println(" Free rectangle at " + cur); + freeListWidth += cur.w(); + } + // Add on the remaining space at the end + System.err.println(" Remaining free space " + (width - nextAddX)); + freeListWidth += (width - nextAddX); + System.err.println(" Total free space " + freeListWidth); + } } diff --git a/src/classes/com/sun/opengl/impl/packrect/LevelSet.java b/src/classes/com/sun/opengl/impl/packrect/LevelSet.java index 561a3fe14..97a1f2e74 100755 --- a/src/classes/com/sun/opengl/impl/packrect/LevelSet.java +++ b/src/classes/com/sun/opengl/impl/packrect/LevelSet.java @@ -149,6 +149,39 @@ public class LevelSet { nextAddY += (newHeight - oldHeight); } + /** Gets the used height of the levels in this LevelSet. */ + public int getUsedHeight() { + return nextAddY; + } + + /** Sets the height of this LevelSet. It is only legal to reduce the + height to greater than or equal to the currently used height. */ + public void setHeight(int height) throws IllegalArgumentException { + if (height < getUsedHeight()) { + throw new IllegalArgumentException("May not reduce height below currently used height"); + } + h = height; + } + + /** Returns the vertical fragmentation ratio of this LevelSet. This + is defined as the ratio of the sum of the heights of all + completely empty Levels divided by the overall used height of + the LevelSet. A high vertical fragmentation ratio indicates that + it may be profitable to perform a compaction. */ + public float verticalFragmentationRatio() { + int freeHeight = 0; + int usedHeight = getUsedHeight(); + if (usedHeight == 0) + return 0.0f; + for (Iterator iter = iterator(); iter.hasNext(); ) { + Level level = (Level) iter.next(); + if (level.isEmpty()) { + freeHeight += level.h(); + } + } + return (float) freeHeight / (float) usedHeight; + } + public Iterator iterator() { return levels.iterator(); } @@ -171,4 +204,10 @@ public class LevelSet { level.updateRectangleReferences(); } } + + /** Clears out all Levels stored in this LevelSet. */ + public void clear() { + levels.clear(); + nextAddY = 0; + } } diff --git a/src/classes/com/sun/opengl/impl/packrect/RectanglePacker.java b/src/classes/com/sun/opengl/impl/packrect/RectanglePacker.java index 453f5e1e2..6057f7bb9 100755 --- a/src/classes/com/sun/opengl/impl/packrect/RectanglePacker.java +++ b/src/classes/com/sun/opengl/impl/packrect/RectanglePacker.java @@ -52,6 +52,13 @@ public class RectanglePacker { private Object backingStore; private LevelSet levels; private float EXPANSION_FACTOR = 0.5f; + private float SHRINK_FACTOR = 0.3f; + + private int initialWidth; + private int initialHeight; + + private int maxWidth = -1; + private int maxHeight = -1; static class RectHComparator implements Comparator { public int compare(Object o1, Object o2) { @@ -71,6 +78,8 @@ public class RectanglePacker { int initialHeight) { this.manager = manager; levels = new LevelSet(initialWidth, initialHeight); + this.initialWidth = initialWidth; + this.initialHeight = initialHeight; } public Object getBackingStore() { @@ -81,6 +90,16 @@ public class RectanglePacker { return backingStore; } + /** Sets up a maximum width and height for the backing store. These + are optional and if not specified the backing store will grow as + necessary. Setting up a maximum width and height introduces the + possibility that additions will fail; these are handled with the + BackingStoreManager's allocationFailed notification. */ + public void setMaxSize(int maxWidth, int maxHeight) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + } + /** Decides upon an (x, y) position for the given rectangle (leaving its width and height unchanged) and places it on the backing store. May provoke re-layout of other Rects already added. */ @@ -89,29 +108,89 @@ public class RectanglePacker { if (backingStore == null) backingStore = manager.allocateBackingStore(levels.w(), levels.h()); - // Try to allocate - if (levels.add(rect)) - return; + int attemptNumber = 0; + boolean tryAgain = false; + + do { + // Try to allocate + if (levels.add(rect)) + return; + + // Try to allocate with horizontal compaction + if (levels.compactAndAdd(rect, backingStore, manager)) + return; + + // Let the manager have a chance at potentially evicting some entries + tryAgain = manager.preExpand(rect, attemptNumber++); + } while (tryAgain); + + compactImpl(rect); + + // Retry the addition of the incoming rectangle + add(rect); + // Done + } + + /** Removes the given rectangle from this RectanglePacker. */ + public void remove(Rect rect) { + levels.remove(rect); + } + + /** Visits all Rects contained in this RectanglePacker. */ + public void visit(RectVisitor visitor) { + levels.visit(visitor); + } - // Try to allocate with compaction - if (levels.compactAndAdd(rect, backingStore, manager)) - return; + /** Returns the vertical fragmentation ratio of this + RectanglePacker. This is defined as the ratio of the sum of the + heights of all completely empty Levels divided by the overall + used height of the LevelSet. A high vertical fragmentation ratio + indicates that it may be profitable to perform a compaction. */ + public float verticalFragmentationRatio() { + return levels.verticalFragmentationRatio(); + } - // Have to expand. Need to figure out what direction to go. Prefer - // to expand vertically. Expand horizontally only if rectangle - // being added is too wide. FIXME: may want to consider - // rebalancing the width and height to be more equal if it turns - // out we keep expanding in the vertical direction. + /** Forces a compaction cycle, which typically results in allocating + a new backing store and copying all entries to it. */ + public void compact() { + compactImpl(null); + } + + // The "cause" rect may be null + private void compactImpl(Rect cause) { + // Have to either expand, compact or both. Need to figure out what + // direction to go. Prefer to expand vertically. Expand + // horizontally only if rectangle being added is too wide. FIXME: + // may want to consider rebalancing the width and height to be + // more equal if it turns out we keep expanding in the vertical + // direction. boolean done = false; int newWidth = levels.w(); int newHeight = levels.h(); LevelSet nextLevelSet = null; + int attemptNumber = 0; + boolean needAdditionFailureNotification = false; + while (!done) { - if (rect.w() > newWidth) { - newWidth = rect.w(); - } else { - newHeight = (int) (newHeight * (1.0f + EXPANSION_FACTOR)); + if (cause != null) { + if (cause.w() > newWidth) { + newWidth = cause.w(); + } else { + newHeight = (int) (newHeight * (1.0f + EXPANSION_FACTOR)); + } } + + // Clamp to maximum values + needAdditionFailureNotification = false; + if (maxWidth > 0 && newWidth > maxWidth) { + newWidth = maxWidth; + needAdditionFailureNotification = true; + } + if (maxHeight > 0 && newHeight > maxHeight) { + newHeight = maxHeight; + needAdditionFailureNotification = true; + } + nextLevelSet = new LevelSet(newWidth, newHeight); // Make copies of all existing rectangles @@ -138,7 +217,42 @@ public class RectanglePacker { break; } } + + if (done && cause != null) { + // Try to add the new rectangle as well + if (nextLevelSet.add(cause)) { + // We're OK + } else { + done = false; + } + } + + // Don't send addition failure notifications if we're only doing + // a compaction + if (!done && needAdditionFailureNotification && cause != null) { + manager.additionFailed(cause, attemptNumber); + } + ++attemptNumber; } + + // See whether the implicit compaction that just occurred has + // yielded excess empty space. + if (nextLevelSet.getUsedHeight() > 0 && + nextLevelSet.getUsedHeight() < nextLevelSet.h() * SHRINK_FACTOR) { + int shrunkHeight = Math.max(initialHeight, + (int) (nextLevelSet.getUsedHeight() * (1.0f + EXPANSION_FACTOR))); + if (maxHeight > 0 && shrunkHeight > maxHeight) { + shrunkHeight = maxHeight; + } + nextLevelSet.setHeight(shrunkHeight); + } + + // If we temporarily added the new rectangle to the new LevelSet, + // take it out since we don't "really" add it here but in add(), above + if (cause != null) { + nextLevelSet.remove(cause); + } + // OK, now we have a new layout and a mapping from the old to the // new locations of rectangles on the backing store. Allocate a // new backing store, move the contents over and deallocate the @@ -162,19 +276,11 @@ public class RectanglePacker { // Update to new versions of backing store and LevelSet backingStore = newBackingStore; levels = nextLevelSet; - // Retry the addition of the incoming rectangle - add(rect); - // Done } - /** Removes the given rectangle from this RectanglePacker. */ - public void remove(Rect rect) { - levels.remove(rect); - } - - /** Visits all Rects contained in this RectanglePacker. */ - public void visit(RectVisitor visitor) { - levels.visit(visitor); + /** Clears all Rects contained in this RectanglePacker. */ + public void clear() { + levels.clear(); } /** Disposes the backing store allocated by the diff --git a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java index 027f857fe..7f47c011a 100755 --- a/src/classes/com/sun/opengl/util/j2d/TextRenderer.java +++ b/src/classes/com/sun/opengl/util/j2d/TextRenderer.java @@ -54,6 +54,13 @@ 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.*; + /** Renders bitmapped Java 2D text into an OpenGL window with high performance, full Unicode support, and a simple API. Performs appropriate caching of text rendering results in an OpenGL texture @@ -94,11 +101,14 @@ import com.sun.opengl.impl.packrect.*; */ public class TextRenderer { + private static final boolean DEBUG = Debug.debug("TextRenderer"); + private Font font; private boolean antialiased; private boolean useFractionalMetrics; private RectanglePacker packer; + private boolean haveMaxSize; private TextureRenderer cachedBackingStore; private Graphics2D cachedGraphics; private FontRenderContext cachedFontRenderContext; @@ -116,8 +126,11 @@ public class TextRenderer { // Every certain number of render cycles, flush the strings which // haven't been used recently - private static final int CYCLES_PER_FLUSH = 200; + 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; // Current text color private float r = 1.0f; @@ -148,6 +161,9 @@ public class TextRenderer { void clearUsed() { used = false; } } + // Debugging purposes only + private boolean debugged; + /** Creates a new TextRenderer with the given font, using no antialiasing or fractional metrics. Equivalent to <code>TextRenderer(font, false, false)</code>. @@ -214,6 +230,23 @@ public class TextRenderer { return normalize(gv.getPixelBounds(frc, 0, 0)); } + /** 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 @@ -229,8 +262,22 @@ public class TextRenderer { @throws GLException If an OpenGL context is not current when this method is called */ public void beginRendering(int width, int height) throws GLException { + if (DEBUG && !debugged) { + debug(); + } + getBackingStore().beginOrthoRendering(width, height); GL gl = GLU.getCurrentGL(); + + 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; + } + // Change texture environment mode to MODULATE gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE); // Change text color to last saved @@ -344,24 +391,10 @@ public class TextRenderer { getBackingStore().endOrthoRendering(); if (++numRenderCycles >= CYCLES_PER_FLUSH) { numRenderCycles = 0; - final 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()); + if (DEBUG) { + System.err.println("Clearing unused entries in endRendering()"); } + clearUnusedEntries(); } } @@ -421,13 +454,6 @@ public class TextRenderer { return cachedGraphics; } - private FontRenderContext getFontRenderContext() { - if (cachedFontRenderContext == null) { - cachedFontRenderContext = getGraphics2D().getFontRenderContext(); - } - return cachedFontRenderContext; - } - private int getSpaceWidth() { if (spaceWidth < 0) { Graphics2D g = getGraphics2D(); @@ -470,7 +496,48 @@ public class TextRenderer { } } - static class Manager implements BackingStoreManager { + private void clearUnusedEntries() { + final 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()); + + if (DEBUG) { + Graphics2D g = getGraphics2D(); + g.setColor(TRANSPARENT_BLACK); + g.fillRect(r.x(), r.y(), r.w(), r.h()); + g.setColor(Color.WHITE); + } + } + + // 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(); + } + + if (DEBUG) { + getBackingStore().sync(0, 0, getBackingStore().getWidth(), getBackingStore().getHeight()); + } + } + + class Manager implements BackingStoreManager { private Graphics2D g; public Object allocateBackingStore(int w, int h) { @@ -479,6 +546,9 @@ public class TextRenderer { // store (i.e., non-default Paint, foreground color, etc.), but // for now, let's just be more efficient TextureRenderer renderer = TextureRenderer.createAlphaOnlyRenderer(w, h); + if (DEBUG) { + System.err.println(" TextRenderer allocating backing store " + w + " x " + h); + } return renderer; } @@ -486,6 +556,43 @@ public class TextRenderer { ((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); + } + clearUnusedEntries(); + return true; + } + + return false; + } + + public void additionFailed(Rect cause, int attemptNumber) { + // Heavy hammer -- might consider doing something different + packer.clear(); + stringLocations.clear(); + + if (DEBUG) { + System.err.println(" *** Cleared all text because addition failed ***"); + } + } + public void beginMovement(Object oldBackingStore, Object newBackingStore) { TextureRenderer newRenderer = (TextureRenderer) newBackingStore; g = newRenderer.createGraphics(); @@ -523,4 +630,65 @@ public class TextRenderer { newRenderer.sync(0, 0, newRenderer.getWidth(), newRenderer.getHeight()); } } + + //---------------------------------------------------------------------- + // Debugging functionality + // + + private void debug() { + Frame 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(256, 256); + 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; + } + + public void display(GLAutoDrawable drawable) { + GL gl = drawable.getGL(); + gl.glClear(GL.GL_DEPTH_BUFFER_BIT | GL.GL_COLOR_BUFFER_BIT); + 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) {} + } } |