changeset 5549:b7d847549c94

Fix RT-32242: Canvas should clear buffer on certain clearRect, fillRect calls Reviewed by: kcr
author flar <James.Graham@oracle.com>
date Fri, 25 Oct 2013 15:24:32 -0700
parents 840c0f5698eb
children cd09e6835158
files modules/graphics/src/main/java/com/sun/javafx/sg/prism/GrowableDataBuffer.java modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java modules/graphics/src/main/java/javafx/scene/canvas/Canvas.java modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java modules/graphics/src/test/java/com/sun/javafx/sg/prism/GrowableDataBufferTest.java
diffstat 5 files changed, 1105 insertions(+), 136 deletions(-) [+]
line wrap: on
line diff
--- a/modules/graphics/src/main/java/com/sun/javafx/sg/prism/GrowableDataBuffer.java	Fri Oct 25 14:07:42 2013 -0700
+++ b/modules/graphics/src/main/java/com/sun/javafx/sg/prism/GrowableDataBuffer.java	Fri Oct 25 15:24:32 2013 -0700
@@ -25,195 +25,559 @@
 
 package com.sun.javafx.sg.prism;
 
+import java.lang.ref.WeakReference;
 import java.nio.BufferOverflowException;
 import java.util.Arrays;
 
 /**
+ * A growable buffer that can contain both byte-encoded primitive values
+ * and a list of Objects stored for communication between a writer that fills
+ * it with data and a reader that empties the data behind the writer.
+ * 
+ * Both buffers (the byte-encoded array and the Object array) grow as needed
+ * with no hard limits and the two are kept separately so it is up to the
+ * reader and writer to read the two streams in a predetermined synchronicity
+ * of the two streams.
+ * 
+ * The methods on a given GrowableDataBuffer object are not synchronized or
+ * thread-safe and writing to or reading from the object from more than one
+ * thread at a time is unsupported.  In particular, multiple writer threads
+ * and/or multiple reader threads will definitely cause problems.
+ * 
+ * The static getBuffer() factory methods and the static returnBuffer() method
+ * are all synchronized so that they can be called from any thread at any
+ * time, but any given buffer should only be returned to the pool once.
  */
-public class GrowableDataBuffer<T> {
-    static final int MIN_BUF_GROW = 1024;
-    static final int MIN_REF_GROW = 32;
+public class GrowableDataBuffer {
+    static final int MIN_VAL_GROW = 1024;
+    static final int MIN_OBJ_GROW = 32;
 
-    byte buf[];
-    T refs[];
-    int pos;
-    int mark;
-    int savepos;
-    int refpos;
-    int refmark;
-    int refsavepos;
+    static class WeakLink {
+        WeakReference<GrowableDataBuffer> bufref;
+        WeakLink next;
+    }
+    static WeakLink buflist = new WeakLink(); // Dummy "head" link object
 
-    public GrowableDataBuffer(int initsize) {
-        buf = new byte[initsize];
+    /**
+     * Retrieve a buffer with an initial byte-encoding capacity of at least
+     * {@code minsize} bytes.
+     * The initial capacity of the object buffer will be the default size.
+     * 
+     * @param minsize the minimum initial size of the byte-encoding buffer
+     * @return a {@code GrowableDataBuffer} object of the requested size
+     */
+    public static GrowableDataBuffer getBuffer(int minsize) {
+        return getBuffer(minsize, MIN_OBJ_GROW);
     }
 
-    public int position() {
-        return pos;
+    /**
+     * Retrieve a buffer with an initial byte-encoding capacity of at least
+     * {@code minvals} bytes and an initial object buffer capacity of at
+     * least {@code minobjs} Objects.
+     * 
+     * @param minvals the minimum initial size of the byte-encoding buffer
+     * @param minobjs the minimum initial size of the Object buffer
+     * @return a {@code GrowableDataBuffer} object of the requested sizes
+     */
+    public synchronized static GrowableDataBuffer getBuffer(int minvals, int minobjs) {
+        WeakLink prev = buflist;
+        WeakLink cur = buflist.next;
+        while (cur != null) {
+            GrowableDataBuffer curgdb = cur.bufref.get();
+            WeakLink next = cur.next;
+            if (curgdb == null) {
+                prev.next = cur = next;
+                continue;
+            }
+            if (curgdb.valueCapacity() >= minvals && curgdb.objectCapacity() >= minobjs) {
+                prev.next = next;
+                return curgdb;
+            }
+            prev = cur;
+            cur = next;
+        }
+        return new GrowableDataBuffer(minvals, minobjs);
     }
 
-    public void save() {
-        savepos = pos;
-        refsavepos = refpos;
+    /**
+     * Return the indicated {@code GrowableDataBuffer} object to the pool
+     * for reuse.
+     * A given {@code GrowableDataBuffer} object should only be returned to
+     * the pool once per retrieval from the {@code getBuffer()} methods.
+     * 
+     * @param gdb the {@code GrowableDataBuffer} object to be reused.
+     */
+    public synchronized static void returnBuffer(GrowableDataBuffer retgdb) {
+        int retvlen = retgdb.valueCapacity();
+        int retolen = retgdb.objectCapacity();
+        retgdb.reset();
+
+        WeakLink prev = buflist;
+        WeakLink cur = buflist.next;
+        while (cur != null) {
+            GrowableDataBuffer curgdb = cur.bufref.get();
+            WeakLink next = cur.next;
+            if (curgdb == null) {
+                prev.next = cur = next;
+                continue;
+            }
+            int curvlen = curgdb.valueCapacity();
+            int curolen = curgdb.objectCapacity();
+            if (curvlen > retvlen ||
+                (curvlen == retvlen && curolen >= retolen))
+            {
+                break;
+            }
+            prev = cur;
+            cur = next;
+        }
+        WeakLink retlink = new WeakLink();
+        retlink.bufref = new WeakReference<>(retgdb);
+        prev.next = retlink;
+        retlink.next = cur;
     }
 
-    public void restore() {
-        pos = savepos;
-        refpos = refsavepos;
+    byte vals[];
+    int writevalpos;     // next vals location to write encoded values
+    int readvalpos;      // next vals location to read encoded values
+    int savevalpos;      // saved valpos for reading data multiple times
+
+    Object objs[];
+    int writeobjpos;     // next objs location to write data objects
+    int readobjpos;      // next objs location to read data objects
+    int saveobjpos;      // saved objpos for reading objects multiple times
+
+    private GrowableDataBuffer(int initvalsize, int initobjsize) {
+        vals = new byte[initvalsize];
+        objs = new Object[initobjsize];
     }
 
-    public boolean isEmpty() {
-        return (pos >= mark && refpos >= refmark);
+    /**
+     * The location of the next byte to be read from the encoded value
+     * buffer.
+     * This must always be less than or equal to the
+     * {@code writeValuePosition()}.
+     * 
+     * @return the byte position of the next byte data to be read.
+     */
+    public int readValuePosition() {
+        return readvalpos;
     }
 
-    public void switchToRead() {
-        mark = pos;
-        refmark = refpos;
-        pos = 0;
-        refpos = 0;
+    /**
+     * The location of the next byte to be written to the encoded value
+     * buffer.
+     * 
+     * @return the byte position of the next byte data to be written.
+     */
+    public int writeValuePosition() {
+        return writevalpos;
     }
 
-    public void resetForWrite() {
-        pos = mark = 0;
-        if (refpos > 0 || refmark > 0) {
-            Arrays.fill(refs, 0, Math.max(refpos, refmark), null);
-            refpos = refmark = 0;
+    /**
+     * The location of the next object to be read from the object buffer.
+     * This must always be less than or equal to the
+     * {@code writeObjectPosition()}.
+     * 
+     * @return the position of the next object to be read.
+     */
+    public int readObjectPosition() {
+        return readobjpos;
+    }
+
+    /**
+     * The location of the next object to be written to the object buffer.
+     * 
+     * @return the position of the next object to be written.
+     */
+    public int writeObjectPosition() {
+        return writeobjpos;
+    }
+
+    /**
+     * The capacity, in bytes, of the byte-encoding buffer.
+     * 
+     * @return the capacity of the byte-encoding buffer
+     */
+    public int valueCapacity() {
+        return vals.length;
+    }
+
+    /**
+     * The capacity, in objects, of the {@code Object} buffer.
+     * 
+     * @return the capacity of the {@code Object} buffer
+     */
+    public int objectCapacity() {
+        return objs.length;
+    }
+
+    /**
+     * Save aside the current read positions of both the byte-encoding
+     * buffer and the {@code Object} buffer for a later {@code restore()}
+     * operation.
+     */
+    public void save() {
+        savevalpos = readvalpos;
+        saveobjpos = readobjpos;
+    }
+
+    /**
+     * Restore the read positions of both the byte-encoding buffer and
+     * the {@code Object} buffer to their last saved positions.
+     */
+    public void restore() {
+        readvalpos = savevalpos;
+        readobjpos = saveobjpos;
+    }
+
+    /**
+     * Indicates whether or not there are values in the byte-encoding
+     * buffer waiting to be read.
+     * 
+     * @return true iff there are data values to be read
+     */
+    public boolean hasValues() {
+        return (readvalpos < writevalpos);
+    }
+
+    /**
+     * Indicates whether or not there are objects in the object
+     * buffer waiting to be read.
+     * 
+     * @return true iff there are objects to be read
+     */
+    public boolean hasObjects() {
+        return (readobjpos < writeobjpos);
+    }
+
+    /**
+     * Indicates whether the byte-encoding buffer is completely empty.
+     * Note that this is different from whether or not there is unread
+     * data in the byte-encoding buffer.  A buffer which has been written
+     * and then later fully emptied by reading is not considered "empty".
+     * 
+     * @return true iff there is no data at all stored in the byte buffer
+     */
+    public boolean isEmpty() {
+        return (writevalpos == 0);
+    }
+
+    /**
+     * Clears out all data and resets all positions to the start of the
+     * buffers so that a new sequence of writing, then reading of data
+     * and objects can begin.
+     * Note that the {@code Object} array is cleared to nulls here and
+     * those objects will finally become collectable by the garbage collector.
+     */
+    public void reset() {
+        readvalpos = savevalpos = writevalpos = 0;
+        readobjpos = saveobjpos = 0;
+        if (writeobjpos > 0) {
+            Arrays.fill(objs, 0, writeobjpos, null);
+            writeobjpos = 0;
         }
     }
 
-    public void ensureWriteCapacity(int newbytes) {
-        if (pos + newbytes > buf.length) {
-            if (newbytes < MIN_BUF_GROW) newbytes = MIN_BUF_GROW;
-            buf = Arrays.copyOf(buf, pos + newbytes);
+    /**
+     * Appends the contents of both the byte and {@code Object} buffers in
+     * the indicated {@code GrowableDataBuffer} to this object.
+     * The data in the other indicated {@code GrowableDataBuffer} object
+     * is not disturbed in any way.
+     * 
+     * @param gdb the {@code GrowableDataBuffer} to append to this object
+     */
+    public void append(GrowableDataBuffer gdb) {
+        ensureWriteCapacity(gdb.writevalpos);
+        System.arraycopy(gdb.vals, 0, vals, writevalpos, gdb.writevalpos);
+        writevalpos += gdb.writevalpos;
+        if (writeobjpos + gdb.writeobjpos > objs.length) {
+            objs = Arrays.copyOf(objs, writeobjpos + gdb.writeobjpos);
+        }
+        System.arraycopy(gdb.objs, 0, objs, writeobjpos, gdb.writeobjpos);
+        writeobjpos += gdb.writeobjpos;
+    }
+
+    private void ensureWriteCapacity(int newbytes) {
+        if (writevalpos + newbytes > vals.length) {
+            if (newbytes < MIN_VAL_GROW) newbytes = MIN_VAL_GROW;
+            vals = Arrays.copyOf(vals, writevalpos + newbytes);
         }
     }
 
-    public void ensureReadCapacity(int bytesneeded) {
-        if (pos + bytesneeded > mark) {
+    private void ensureReadCapacity(int bytesneeded) {
+        if (readvalpos + bytesneeded > writevalpos) {
             throw new BufferOverflowException();
         }
     }
 
+    /**
+     * Encode a boolean value and write it to the end of the byte-encoding array
+     * 
+     * @param b the boolean value to be written
+     */
     public void putBoolean(boolean b) {
         putByte(b ? (byte) 1 : (byte) 0);
     }
 
+    /**
+     * Write a byte value to the end of the byte-encoding array
+     * 
+     * @param b the byte value to be written
+     */
     public void putByte(byte b) {
         ensureWriteCapacity(1);
-        buf[pos++] = b;
+        vals[writevalpos++] = b;
     }
 
+    /**
+     * Encode a char value and write it to the end of the byte-encoding array
+     * 
+     * @param c the char value to be written
+     */
     public void putChar(char c) {
         ensureWriteCapacity(2);
-        buf[pos++] = (byte) (c >>  8);
-        buf[pos++] = (byte) (c      );
+        vals[writevalpos++] = (byte) (c >>  8);
+        vals[writevalpos++] = (byte) (c      );
     }
 
+    /**
+     * Encode a short value and write it to the end of the byte-encoding array
+     * 
+     * @param s the short value to be written
+     */
     public void putShort(short s) {
         ensureWriteCapacity(2);
-        buf[pos++] = (byte) (s >>  8);
-        buf[pos++] = (byte) (s      );
+        vals[writevalpos++] = (byte) (s >>  8);
+        vals[writevalpos++] = (byte) (s      );
     }
 
+    /**
+     * Encode an int value and write it to the end of the byte-encoding array
+     * 
+     * @param i the int value to be written
+     */
     public void putInt(int i) {
         ensureWriteCapacity(4);
-        buf[pos++] = (byte) (i >> 24);
-        buf[pos++] = (byte) (i >> 16);
-        buf[pos++] = (byte) (i >>  8);
-        buf[pos++] = (byte) (i      );
+        vals[writevalpos++] = (byte) (i >> 24);
+        vals[writevalpos++] = (byte) (i >> 16);
+        vals[writevalpos++] = (byte) (i >>  8);
+        vals[writevalpos++] = (byte) (i      );
     }
 
+    /**
+     * Encode a long value and write it to the end of the byte-encoding array
+     * 
+     * @param l the long value to be written
+     */
     public void putLong(long l) {
         ensureWriteCapacity(8);
-        buf[pos++] = (byte) (l >> 56);
-        buf[pos++] = (byte) (l >> 48);
-        buf[pos++] = (byte) (l >> 40);
-        buf[pos++] = (byte) (l >> 32);
-        buf[pos++] = (byte) (l >> 24);
-        buf[pos++] = (byte) (l >> 16);
-        buf[pos++] = (byte) (l >>  8);
-        buf[pos++] = (byte) (l      );
+        vals[writevalpos++] = (byte) (l >> 56);
+        vals[writevalpos++] = (byte) (l >> 48);
+        vals[writevalpos++] = (byte) (l >> 40);
+        vals[writevalpos++] = (byte) (l >> 32);
+        vals[writevalpos++] = (byte) (l >> 24);
+        vals[writevalpos++] = (byte) (l >> 16);
+        vals[writevalpos++] = (byte) (l >>  8);
+        vals[writevalpos++] = (byte) (l      );
     }
 
+    /**
+     * Encode a float value and write it to the end of the byte-encoding array
+     * 
+     * @param f the float value to be written
+     */
     public void putFloat(float f) {
         putInt(Float.floatToIntBits(f));
     }
 
+    /**
+     * Encode a double value and write it to the end of the byte-encoding array
+     * 
+     * @param d the double value to be written
+     */
     public void putDouble(double d) {
         putLong(Double.doubleToLongBits(d));
     }
 
-    public void putObject(T o) {
-        if (refs == null) {
-            refs = (T[]) new Object[MIN_REF_GROW];
-        } else if (refpos >= refs.length) {
-            refs = Arrays.copyOf(refs, refpos+MIN_REF_GROW);
+    /**
+     * Write an {@code Object} to the end of the object array
+     * 
+     * @param o the {@code Object} to be written
+     */
+    public void putObject(Object o) {
+        if (writeobjpos >= objs.length) {
+            objs = Arrays.copyOf(objs, writeobjpos+MIN_OBJ_GROW);
         }
-        refs[refpos++] = o;
+        objs[writeobjpos++] = o;
     }
 
+    /**
+     * Read a single byte from the byte-encoded stream, ignoring any read
+     * position, but honoring the current write position as a limit.
+     * The read and saved positions are not used or modified in any way
+     * by this method
+     * 
+     * @param i the absolute byte location to return from the byte-encoding array
+     * @return the byte stored at the indicated location in the byte array
+     */
+    public byte peekByte(int i) {
+        if (i >= writevalpos) {
+            throw new BufferOverflowException();
+        }
+        return vals[i];
+    }
+
+    /**
+     * Read a single {@code Object} from the object buffer, ignoring any read
+     * position, but honoring the current write position as a limit.
+     * The read and saved positions are not used or modified in any way
+     * by this method
+     * 
+     * @param i the absolute index to return from the {@code Object} array
+     * @return the {@code Object} stored at the indicated index
+     */
+    public Object peekObject(int i) {
+        if (i >= writeobjpos) {
+            throw new BufferOverflowException();
+        }
+        return objs[i];
+    }
+
+    /**
+     * Decodes and returns a single boolean value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded boolean value
+     */
     public boolean getBoolean() {
         ensureReadCapacity(1);
-        return buf[pos++] != 0;
+        return vals[readvalpos++] != 0;
     }
 
+    /**
+     * Returns a single byte value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the returned value.
+     * 
+     * @return the decoded byte value
+     */
     public byte getByte() {
         ensureReadCapacity(1);
-        return buf[pos++];
+        return vals[readvalpos++];
     }
 
+    /**
+     * Decodes a single unsigned byte value from the current read
+     * position in the byte-encoded stream and returns the value cast to
+     * an int and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded unsigned byte value as an int
+     */
     public int getUByte() {
         ensureReadCapacity(1);
-        return buf[pos++] & 0xff;
+        return vals[readvalpos++] & 0xff;
     }
 
+    /**
+     * Decodes and returns a single char value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded char value
+     */
     public char getChar() {
         ensureReadCapacity(2);
-        int c = buf[pos++];
-        c = (c << 8) | (buf[pos++] & 0xff);
+        int c = vals[readvalpos++];
+        c = (c << 8) | (vals[readvalpos++] & 0xff);
         return (char) c;
     }
 
+    /**
+     * Decodes and returns a single short value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded short value
+     */
     public short getShort() {
         ensureReadCapacity(2);
-        int s = buf[pos++];
-        s = (s << 8) | (buf[pos++] & 0xff);
+        int s = vals[readvalpos++];
+        s = (s << 8) | (vals[readvalpos++] & 0xff);
         return (short) s;
     }
 
+    /**
+     * Decodes and returns a single int value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded int value
+     */
     public int getInt() {
         ensureReadCapacity(4);
-        int i = buf[pos++];
-        i = (i << 8) | (buf[pos++] & 0xff);
-        i = (i << 8) | (buf[pos++] & 0xff);
-        i = (i << 8) | (buf[pos++] & 0xff);
+        int i = vals[readvalpos++];
+        i = (i << 8) | (vals[readvalpos++] & 0xff);
+        i = (i << 8) | (vals[readvalpos++] & 0xff);
+        i = (i << 8) | (vals[readvalpos++] & 0xff);
         return i;
     }
 
+    /**
+     * Decodes and returns a single long value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded long value
+     */
     public long getLong() {
         ensureReadCapacity(8);
-        long l = buf[pos++];
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
-        l = (l << 8) | (buf[pos++] & 0xff);
+        long l = vals[readvalpos++];
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
+        l = (l << 8) | (vals[readvalpos++] & 0xff);
         return l;
     }
 
+    /**
+     * Decodes and returns a single float value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded float value
+     */
     public float getFloat() {
         return Float.intBitsToFloat(getInt());
     }
 
+    /**
+     * Decodes and returns a single double value from the current read
+     * position in the byte-encoded stream and bumps the read position
+     * past the decoded value.
+     * 
+     * @return the decoded double value
+     */
     public double getDouble() {
         return Double.longBitsToDouble(getLong());
     }
 
-    public T getObject() {
-        if (refs == null || refpos >= refs.length) {
+    /**
+     * Returns a single {@code Object} from the current object read
+     * position in the {@code Object} stream and bumps the read position
+     * past the returned value.
+     * 
+     * @return the {@code Object} read from the buffer
+     */
+    public Object getObject() {
+        if (readobjpos >= objs.length) {
             throw new BufferOverflowException();
         }
-        return refs[refpos++];
+        return objs[readobjpos++];
     }
 }
--- a/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java	Fri Oct 25 14:07:42 2013 -0700
+++ b/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java	Fri Oct 25 15:24:32 2013 -0700
@@ -119,6 +119,10 @@
     public static final byte                   FX_BASE = 60;
     public static final byte FX_APPLY_EFFECT = FX_BASE + 0;
 
+    public static final byte                   UTIL_BASE = 70;
+    public static final byte RESET           = UTIL_BASE + 0;
+    public static final byte SET_DIMS        = UTIL_BASE + 1;
+
     public static final byte CAP_BUTT   = 0;
     public static final byte CAP_ROUND  = 1;
     public static final byte CAP_SQUARE = 2;
@@ -292,15 +296,15 @@
     private static Image TMP_IMAGE = Image.fromIntArgbPreData(new int[1], 1, 1);
     private static Blend BLENDER = new MyBlend(Mode.SRC_OVER, null, null);
 
-    private GrowableDataBuffer<Object> thebuf;
+    private GrowableDataBuffer thebuf;
 
     private int tw, th;
+    private int cw, ch;
     private RenderBuf cv;
     private RenderBuf temp;
     private RenderBuf clip;
 
     private float globalAlpha;
-    private byte fillRule;
     private Blend.Mode blendmode;
     private Paint fillPaint, strokePaint;
     private float linewidth;
@@ -329,8 +333,16 @@
         temp = new RenderBuf(InitType.CLEAR);
         clip = new RenderBuf(InitType.FILL_WHITE);
 
+        path = new Path2D();
+        ngtext = new NGText();
+        textLayout = new PrismTextLayout();
+        transform = new Affine2D();
+        clipStack = new LinkedList<Path2D>();
+        initAttributes();
+    }
+
+    private void initAttributes() {
         globalAlpha = 1.0f;
-        fillRule = FILL_RULE_NON_ZERO;
         blendmode = Mode.SRC_OVER;
         fillPaint = Color.BLACK;
         strokePaint = Color.BLACK;
@@ -339,15 +351,14 @@
         linejoin = BasicStroke.JOIN_MITER;
         miterlimit = 10f;
         stroke = null;
-        path = new Path2D();
-        ngtext = new NGText();
-        textLayout = new PrismTextLayout();
+        path.setWindingRule(Path2D.WIND_NON_ZERO);
+        // ngtext stores no state between render operations
+        // textLayout stores no state between render operations
         pgfont = (PGFont) Font.getDefault().impl_getNativeFont();
         align = ALIGN_LEFT;
         baseline = VPos.BASELINE.ordinal();
-        transform = new Affine2D(highestPixelScale, 0, 0,
-                                 highestPixelScale, 0, 0);
-        clipStack = new LinkedList<Path2D>();
+        transform.setToScale(highestPixelScale, highestPixelScale);
+        clipStack.clear();
     }
 
     static final Affine2D TEMP_PATH_TX = new Affine2D();
@@ -507,9 +518,8 @@
         initCanvas(g);
         if (cv.tex != null) {
             if (thebuf != null) {
-                thebuf.switchToRead();
                 renderStream(thebuf);
-                thebuf.resetForWrite();
+                GrowableDataBuffer.returnBuffer(thebuf);
                 thebuf = null;
             }
             float dw = tw / highestPixelScale;
@@ -537,6 +547,14 @@
         }
     }
 
+    private void clearCanvas(int x, int y, int w, int h) {
+        cv.g.setCompositeMode(CompositeMode.SRC);
+        cv.g.setTransform(BaseTransform.IDENTITY_TRANSFORM);
+        cv.g.setPaint(Color.TRANSPARENT);
+        cv.g.fillRect(x, y, w, h);
+        cv.g.setCompositeMode(CompositeMode.SRC_OVER);
+    }
+
     private void initClip() {
         if (clip.validate(cv.g, tw, th)) {
             clip.tex.contentsUseful();
@@ -635,9 +653,39 @@
     };
     private static final Affine2D TEMP_TX = new Affine2D();
     private void renderStream(GrowableDataBuffer buf) {
-        while (!buf.isEmpty()) {
+        while (buf.hasValues()) {
             int token = buf.getByte();
             switch (token) {
+                case RESET:
+                    initAttributes();
+                    // RESET is always followed by SET_DIMS
+                    // Setting cwh = twh avoids unnecessary double clears
+                    this.cw = this.tw;
+                    this.ch = this.th;
+                    clearCanvas(0, 0, this.tw, this.th);
+                    break;
+                case SET_DIMS:
+                    int neww = (int) Math.ceil(buf.getFloat() * highestPixelScale);
+                    int newh = (int) Math.ceil(buf.getFloat() * highestPixelScale);
+                    int clearx = Math.min(neww, this.cw);
+                    int cleary = Math.min(newh, this.ch);
+                    if (clearx < this.tw) {
+                        // tw is set to the final width, we simulate all of
+                        // the intermediate changes in size by making sure
+                        // that all pixels outside of any size change are
+                        // cleared at the stream point where they happened
+                        clearCanvas(clearx, 0, this.tw-clearx, this.th);
+                    }
+                    if (cleary < this.th) {
+                        // th is set to the final width, we simulate all of
+                        // the intermediate changes in size by making sure
+                        // that all pixels outside of any size change are
+                        // cleared at the stream point where they happened
+                        clearCanvas(0, cleary, this.tw, this.th-cleary);
+                    }
+                    this.cw = neww;
+                    this.ch = newh;
+                    break;
                 case PATHSTART:
                     path.reset();
                     break;
@@ -759,8 +807,7 @@
                     globalAlpha = buf.getFloat();
                     break;
                 case FILL_RULE:
-                    fillRule = buf.getByte();
-                    if (fillRule == FILL_RULE_NON_ZERO) {
+                    if (buf.getByte() == FILL_RULE_NON_ZERO) {
                         path.setWindingRule(Path2D.WIND_NON_ZERO);
                     } else {
                         path.setWindingRule(Path2D.WIND_EVEN_ODD);
@@ -1278,18 +1325,40 @@
         geometryChanged();
     }
 
-    public void updateRendering(GrowableDataBuffer buf) {
-        this.thebuf = buf;
+    // Returns true if we are falling behind in rendering (i.e. we
+    // have unrendered data at the time of the synch.  This tells
+    // the FX layer that it should consider emitting a RESET if it
+    // detects a full-canvas clear command even if it looks like it
+    // is superfluous.
+    public boolean updateRendering(GrowableDataBuffer buf) {
+        if (buf.isEmpty()) {
+            GrowableDataBuffer.returnBuffer(buf);
+            return (this.thebuf != null);
+        }
+        boolean reset = (buf.peekByte(0) == RESET);
+        GrowableDataBuffer retbuf;
+        if (reset || this.thebuf == null) {
+            retbuf = this.thebuf;
+            this.thebuf = buf;
+        } else {
+            this.thebuf.append(buf);
+            retbuf = buf;
+        }
         geometryChanged();
+        if (retbuf != null) {
+            GrowableDataBuffer.returnBuffer(retbuf);
+            return true;
+        }
+        return false;
     }
 
     class RenderInput extends Effect {
         float x, y, w, h;
         int token;
-        GrowableDataBuffer<Object> buf;
+        GrowableDataBuffer buf;
         Affine2D savedBoundsTx = new Affine2D();
 
-        public RenderInput(int token, GrowableDataBuffer<Object> buf,
+        public RenderInput(int token, GrowableDataBuffer buf,
                            BaseTransform boundsTx, RectBounds rb)
         {
             this.token = token;
--- a/modules/graphics/src/main/java/javafx/scene/canvas/Canvas.java	Fri Oct 25 14:07:42 2013 -0700
+++ b/modules/graphics/src/main/java/javafx/scene/canvas/Canvas.java	Fri Oct 25 15:24:32 2013 -0700
@@ -72,10 +72,15 @@
  * @since JavaFX 2.2
  */
 public class Canvas extends Node {
-    private static final int DEFAULT_BUF_SIZE = 1024;
+    static final int DEFAULT_VAL_BUF_SIZE = 1024;
+    static final int DEFAULT_OBJ_BUF_SIZE = 32;
+    private static final int SIZE_HISTORY = 5;
 
-    private GrowableDataBuffer<Object> empty;
-    private GrowableDataBuffer<Object> full;
+    private GrowableDataBuffer current;
+    private boolean rendererBehind;
+    private int recentvalsizes[];
+    private int recentobjsizes[];
+    private int lastsizeindex;
 
     private GraphicsContext theContext;
 
@@ -93,17 +98,33 @@
      * @param height height of the canvas
      */
     public Canvas(double width, double height) {
+        this.recentvalsizes = new int[SIZE_HISTORY];
+        this.recentobjsizes = new int[SIZE_HISTORY];
         setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
         setWidth(width);
         setHeight(height);
     }
 
-    GrowableDataBuffer<Object> getBuffer() {
+    private static int max(int sizes[], int defsize) {
+        for (int s : sizes) {
+            if (defsize < s) defsize = s;
+        }
+        return defsize;
+    }
+
+    GrowableDataBuffer getBuffer() {
         impl_markDirty(DirtyBits.NODE_CONTENTS);
-        if (empty == null) {
-            empty = new GrowableDataBuffer<Object>(DEFAULT_BUF_SIZE);
+        if (current == null) {
+            int vsize = max(recentvalsizes, DEFAULT_VAL_BUF_SIZE);
+            int osize = max(recentobjsizes, DEFAULT_OBJ_BUF_SIZE);
+            current = GrowableDataBuffer.getBuffer(vsize, osize);
+            theContext.updateDimensions();
         }
-        return empty;
+        return current;
+    }
+
+    boolean isRendererFallingBehind() {
+        return rendererBehind;
     }
 
     /**
@@ -148,6 +169,14 @@
                 }
 
                 @Override
+                public void set(double newValue) {
+                    super.set(newValue);
+                    if (theContext != null) {
+                        theContext.updateDimensions();
+                    }
+                }
+
+                @Override
                 public String getName() {
                     return "width";
                 }
@@ -188,6 +217,14 @@
                 }
 
                 @Override
+                public void set(double newValue) {
+                    super.set(newValue);
+                    if (theContext != null) {
+                        theContext.updateDimensions();
+                    }
+                }
+
+                @Override
                 public String getName() {
                     return "height";
                 }
@@ -220,17 +257,18 @@
         }
         if (impl_isDirty(DirtyBits.NODE_CONTENTS)) {
             NGCanvas peer = impl_getPeer();
-            if (empty != null && empty.position() > 0) {
-                 peer.updateRendering(empty);
-                 if (full != null) {
-                    full.resetForWrite();
-                 }
-                 GrowableDataBuffer tmp = empty;
-                 empty = full;
-                 full = tmp;
+            if (current != null && !current.isEmpty()) {
+                if (--lastsizeindex < 0) {
+                    lastsizeindex = SIZE_HISTORY - 1;
+                }
+                recentvalsizes[lastsizeindex] = current.writeValuePosition();
+                recentobjsizes[lastsizeindex] = current.writeObjectPosition();
+                rendererBehind = peer.updateRendering(current);
+                current = null;
             }
         }
     }
+
     /**
      * @treatAsPrivate implementation detail
      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
--- a/modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java	Fri Oct 25 14:07:42 2013 -0700
+++ b/modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java	Fri Oct 25 15:24:32 2013 -0700
@@ -109,7 +109,7 @@
     State curState;
     LinkedList<State> stateStack;
     LinkedList<Path2D> clipStack;
-  
+
     GraphicsContext(Canvas theCanvas) {
         this.theCanvas = theCanvas;
         this.path = new Path2D();
@@ -138,22 +138,26 @@
         FillRule fillRule;
 
         State() {
-            this(1.0, BlendMode.SRC_OVER,
-                 new Affine2D(),
-                 Color.BLACK, Color.BLACK,
-                 1.0, StrokeLineCap.SQUARE, StrokeLineJoin.MITER, 10.0,
-                 0, Font.getDefault(), TextAlignment.LEFT, VPos.BASELINE,
-                 null, FillRule.NON_ZERO);
+            init();
+        }
+
+        final void init() {
+            set(1.0, BlendMode.SRC_OVER,
+                new Affine2D(),
+                Color.BLACK, Color.BLACK,
+                1.0, StrokeLineCap.SQUARE, StrokeLineJoin.MITER, 10.0,
+                0, Font.getDefault(), TextAlignment.LEFT, VPos.BASELINE,
+                null, FillRule.NON_ZERO);
         }
 
         State(State copy) {
-            this(copy.globalAlpha, copy.blendop,
-                 new Affine2D(copy.transform),
-                 copy.fill, copy.stroke,
-                 copy.linewidth, copy.linecap, copy.linejoin, copy.miterlimit,
-                 copy.numClipPaths,
-                 copy.font, copy.textalign, copy.textbaseline,
-                 copy.effect, copy.fillRule);
+            set(copy.globalAlpha, copy.blendop,
+                new Affine2D(copy.transform),
+                copy.fill, copy.stroke,
+                copy.linewidth, copy.linecap, copy.linejoin, copy.miterlimit,
+                copy.numClipPaths,
+                copy.font, copy.textalign, copy.textbaseline,
+                copy.effect, copy.fillRule);
         }
 
         State(double globalAlpha, BlendMode blendop,
@@ -164,6 +168,23 @@
                      Font font, TextAlignment align, VPos baseline,
                      Effect effect, FillRule fillRule)
         {
+            set(globalAlpha, blendop,
+                new Affine2D(transform),
+                fill, stroke,
+                linewidth, linecap, linejoin, miterlimit,
+                numClipPaths,
+                font, textalign, textbaseline,
+                effect, fillRule);
+        }
+
+        final void set(double globalAlpha, BlendMode blendop,
+                       Affine2D transform, Paint fill, Paint stroke,
+                       double linewidth, StrokeLineCap linecap,
+                       StrokeLineJoin linejoin, double miterlimit,
+                       int numClipPaths,
+                       Font font, TextAlignment align, VPos baseline,
+                       Effect effect, FillRule fillRule)
+        {
             this.globalAlpha = globalAlpha;
             this.blendop = blendop;
             this.transform = transform;
@@ -376,7 +397,7 @@
         buf.putObject(text);
     }
 
-    private void writeParam(double v, byte command) {
+    void writeParam(double v, byte command) {
         GrowableDataBuffer buf = getBuffer();
         buf.putByte(command);
         buf.putFloat((float) v);
@@ -402,8 +423,68 @@
             buf.putDouble(curState.transform.getMyt());
         }
     }
-    
-   /**
+
+    void updateDimensions() {
+        GrowableDataBuffer buf = getBuffer();
+        buf.putByte(NGCanvas.SET_DIMS);
+        buf.putFloat((float) theCanvas.getWidth());
+        buf.putFloat((float) theCanvas.getHeight());
+    }
+
+    private void reset() {
+        GrowableDataBuffer buf = getBuffer();
+        // Only reset if we have a significant amount of data to omit,
+        // this prevents a common occurence of "setFill(bg); fillRect();"
+        // at the start of a session from invoking a reset.
+        // But, do a reset anyway if the rendering layer has been falling
+        // behind because that lets the synchronization step throw out the
+        // older buffers that have been backing up.
+        if (buf.writeValuePosition() > Canvas.DEFAULT_VAL_BUF_SIZE ||
+            theCanvas.isRendererFallingBehind())
+        {
+            buf.reset();
+            buf.putByte(NGCanvas.RESET);
+            updateDimensions();
+            txdirty = true;
+            pathDirty = true;
+            State s = this.curState;
+            int numClipPaths = this.curState.numClipPaths;
+            this.curState = new State();
+            for (int i = 0; i < numClipPaths; i++) {
+                Path2D clip = clipStack.get(i);
+                buf.putByte(NGCanvas.PUSH_CLIP);
+                buf.putObject(clip);
+            }
+            this.curState.numClipPaths = numClipPaths;
+            s.restore(this);
+        }
+    }
+
+    private void resetIfCovers(Paint p, double x, double y, double w, double h) {
+        Affine2D tx = this.curState.transform;
+        if (tx.isTranslateOrIdentity()) {
+            x += tx.getMxt();
+            y += tx.getMyt();
+            if (x > 0 || y > 0 ||
+                (x+w) < theCanvas.getWidth() ||
+                (y+h) < theCanvas.getHeight())
+            {
+                return;
+            }
+        } else {
+//          quad test for coverage...?
+            return;
+        }
+        if (p != null) {
+            if (this.curState.blendop != BlendMode.SRC_OVER) return;
+            if (!p.isOpaque() || this.curState.globalAlpha < 1.0) return;
+        }
+        if (this.curState.numClipPaths > 0) return;
+        if (this.curState.effect != null) return;
+        reset();
+    }
+
+    /**
     * Gets the {@code Canvas} that the {@code GraphicsContext} is issuing draw
     * commands to. There is only ever one {@code Canvas} for a 
     * {@code GraphicsContext}.
@@ -1426,6 +1507,7 @@
      */
     public void clearRect(double x, double y, double w, double h) {
         if (w != 0 && h != 0) {
+            resetIfCovers(null, x, y, w, h);
             writeOp4(x, y, w, h, NGCanvas.CLEAR_RECT);
         }
     }
@@ -1440,6 +1522,7 @@
      */
     public void fillRect(double x, double y, double w, double h) {
         if (w != 0 && h != 0) {
+            resetIfCovers(this.curState.fill, x, y, w, h);
             writeOp4(x, y, w, h, NGCanvas.FILL_RECT);
         }
     }
@@ -1722,8 +1805,8 @@
                                           PixelFormat pf, int scan)
                 {
                     // assert (w >= 0 && h >= 0) - checked by caller
-                    int cw = (int) theCanvas.getWidth();
-                    int ch = (int) theCanvas.getHeight();
+                    int cw = (int) Math.ceil(theCanvas.getWidth());
+                    int ch = (int) Math.ceil(theCanvas.getHeight());
                     if (x >= 0 && y >= 0 && x+w <= cw && y+h <= ch) {
                         return null;
                     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/graphics/src/test/java/com/sun/javafx/sg/prism/GrowableDataBufferTest.java	Fri Oct 25 15:24:32 2013 -0700
@@ -0,0 +1,415 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.sg.prism;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ */
+public class GrowableDataBufferTest {
+
+    public boolean encodableBooleans[] = {
+        false, true
+    };
+
+    public char encodableChars[] = {
+        ' ', 'A', 'Z', 'a', 'z', '0', '9'
+    };
+
+    public byte encodableBytes[] = {
+        -1, 0, 1, Byte.MIN_VALUE, Byte.MAX_VALUE
+    };
+
+    public short encodableShorts[] = {
+        -1, 0, 1, Short.MIN_VALUE, Short.MAX_VALUE
+    };
+
+    public int encodableInts[] = {
+        -1, 0, 1, Integer.MIN_VALUE, Integer.MAX_VALUE
+    };
+
+    public long encodableLongs[] = {
+        -1L, 0L, 1L, Long.MIN_VALUE, Long.MAX_VALUE
+    };
+
+    public float encodableFloats[] = {
+        -1.0f, 0.0f, 1.0f, Float.MIN_VALUE, Float.MAX_VALUE,
+        Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY
+    };
+
+    public double encodableDoubles[] = {
+        -1.0, 0.0, 1.0, Double.MIN_VALUE, Double.MAX_VALUE,
+        Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY
+    };
+
+    int NUM_VALUES =
+            encodableBooleans.length +
+            encodableChars.length +
+            encodableBytes.length +
+            encodableShorts.length +
+            encodableInts.length +
+            encodableLongs.length +
+            encodableFloats.length +
+            encodableDoubles.length;
+    int NUM_BYTES =
+            encodableBooleans.length +
+            encodableChars.length * 2 +
+            encodableBytes.length +
+            encodableShorts.length * 2 +
+            encodableInts.length * 4 +
+            encodableLongs.length * 8 +
+            encodableFloats.length * 4 +
+            encodableDoubles.length * 8;
+
+    void putBooleans(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (boolean b : encodableBooleans) {
+            if (putPrim) gdb.putBoolean(b);
+            if (putObj) gdb.putObject(Boolean.valueOf(b));
+        }
+    }
+
+    void getBooleans(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (boolean b : encodableBooleans) {
+            if (getPrim) assertTrue(gdb.getBoolean() == b);
+            if (getObj) assertTrue(gdb.getObject().equals(Boolean.valueOf(b)));
+        }
+    }
+
+    void putChars(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (char c : encodableChars) {
+            if (putPrim) gdb.putChar(c);
+            if (putObj) gdb.putObject(Character.valueOf(c));
+        }
+    }
+
+    void getChars(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (char c : encodableChars) {
+            if (getPrim) assertTrue(gdb.getChar() == c);
+            if (getObj) assertTrue(gdb.getObject().equals(Character.valueOf(c)));
+        }
+    }
+
+    void putBytes(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (byte b : encodableBytes) {
+            if (putPrim) gdb.putByte(b);
+            if (putObj) gdb.putObject(Byte.valueOf(b));
+        }
+    }
+
+    void getBytes(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (byte b : encodableBytes) {
+            if (getPrim) assertTrue(gdb.getByte() == b);
+            if (getObj) assertTrue(gdb.getObject().equals(Byte.valueOf(b)));
+        }
+    }
+
+    void putUBytes(GrowableDataBuffer gdb) {
+        for (byte b : encodableBytes) {
+            gdb.putByte(b);
+        }
+    }
+
+    void getUBytes(GrowableDataBuffer gdb) {
+        for (byte b : encodableBytes) {
+            assertTrue(gdb.getUByte() == (b & 0xff));
+        }
+    }
+
+    void putShorts(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (short s : encodableShorts) {
+            if (putPrim) gdb.putShort(s);
+            if (putObj) gdb.putObject(Short.valueOf(s));
+        }
+    }
+
+    void getShorts(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (short s : encodableShorts) {
+            if (getPrim) assertTrue(gdb.getShort() == s);
+            if (getObj) assertTrue(gdb.getObject().equals(Short.valueOf(s)));
+        }
+    }
+
+    void putInts(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (int i : encodableInts) {
+            if (putPrim) gdb.putInt(i);
+            if (putObj) gdb.putObject(Integer.valueOf(i));
+        }
+    }
+
+    void getInts(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (int i : encodableInts) {
+            if (getPrim) assertTrue(gdb.getInt() == i);
+            if (getObj) assertTrue(gdb.getObject().equals(Integer.valueOf(i)));
+        }
+    }
+
+    void putLongs(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (long l : encodableLongs) {
+            if (putPrim) gdb.putLong(l);
+            if (putObj) gdb.putObject(Long.valueOf(l));
+        }
+    }
+
+    void getLongs(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (long c : encodableLongs) {
+            if (getPrim) assertTrue(gdb.getLong() == c);
+            if (getObj) assertTrue(gdb.getObject().equals(Long.valueOf(c)));
+        }
+    }
+
+    void putFloats(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (float f : encodableFloats) {
+            if (putPrim) gdb.putFloat(f);
+            if (putObj) gdb.putObject(Float.valueOf(f));
+        }
+    }
+
+    void getFloats(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (float f : encodableFloats) {
+            if (getPrim) assertTrue(gdb.getFloat() == f);
+            if (getObj) assertTrue(gdb.getObject().equals(Float.valueOf(f)));
+        }
+    }
+
+    void putFloatNaN(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        if (putPrim) gdb.putFloat(Float.NaN);
+        if (putObj) gdb.putObject(Float.valueOf(Float.NaN));
+    }
+
+    void getFloatNaN(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        if (getPrim) assertTrue(Float.isNaN(gdb.getFloat()));
+        if (getObj) assertTrue(gdb.getObject().equals(Float.valueOf(Float.NaN)));
+    }
+
+    void putDoubles(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        for (double d : encodableDoubles) {
+            if (putPrim) gdb.putDouble(d);
+            if (putObj) gdb.putObject(Double.valueOf(d));
+        }
+    }
+
+    void getDoubles(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        for (double d : encodableDoubles) {
+            if (getPrim) assertTrue(gdb.getDouble() == d);
+            if (getObj) assertTrue(gdb.getObject().equals(Double.valueOf(d)));
+        }
+    }
+
+    void putDoubleNaN(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        if (putPrim) gdb.putDouble(Double.NaN);
+        if (putObj) gdb.putObject(Double.valueOf(Double.NaN));
+    }
+
+    void getDoubleNaN(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        if (getPrim) assertTrue(Double.isNaN(gdb.getDouble()));
+        if (getObj) assertTrue(gdb.getObject().equals(Double.valueOf(Double.NaN)));
+    }
+
+    void fill(GrowableDataBuffer gdb, boolean putPrim, boolean putObj) {
+        putBooleans(gdb, putPrim, putObj);
+        putChars(gdb, putPrim, putObj);
+        putBytes(gdb, putPrim, putObj);
+        putShorts(gdb, putPrim, putObj);
+        putInts(gdb, putPrim, putObj);
+        putLongs(gdb, putPrim, putObj);
+        putFloats(gdb, putPrim, putObj);
+        putFloatNaN(gdb, putPrim, putObj);
+        putDoubles(gdb, putPrim, putObj);
+        putDoubleNaN(gdb, putPrim, putObj);
+    }
+
+    void test(GrowableDataBuffer gdb, boolean getPrim, boolean getObj) {
+        getBooleans(gdb, getPrim, getObj);
+        getChars(gdb, getPrim, getObj);
+        getBytes(gdb, getPrim, getObj);
+        getShorts(gdb, getPrim, getObj);
+        getInts(gdb, getPrim, getObj);
+        getLongs(gdb, getPrim, getObj);
+        getFloats(gdb, getPrim, getObj);
+        getFloatNaN(gdb, getPrim, getObj);
+        getDoubles(gdb, getPrim, getObj);
+        getDoubleNaN(gdb, getPrim, getObj);
+    }
+
+    @Test public void testCapacities() {
+        for (int i = 1; i < 100000; i += 100) {
+            GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(i, i);
+            assertFalse(gdb.hasValues());
+            assertFalse(gdb.hasObjects());
+            assertTrue(gdb.isEmpty());
+            assertTrue(gdb.valueCapacity() >= i);
+            assertTrue(gdb.objectCapacity() >= i);
+        }
+    }
+
+    @Test public void testWriteAndReadValues() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, true, false);
+        assertTrue(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+        test(gdb, true, false);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testWriteAndReadUbytes() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        putUBytes(gdb);
+        assertTrue(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+        getUBytes(gdb);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testWriteAndReadObjects() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, false, true);
+        assertFalse(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, false, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testWriteAndReadValuesAndObjects() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, true, true);
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, true, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testWriteAndReadGrowableValues() {
+        for (int i = 0; i < NUM_BYTES; i++) {
+            GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(i, NUM_VALUES);
+            fill(gdb, true, false);
+            assertTrue(gdb.hasValues());
+            assertFalse(gdb.hasObjects());
+            test(gdb, true, false);
+            assertFalse(gdb.hasValues());
+            assertFalse(gdb.hasObjects());
+        }
+    }
+
+    @Test public void testWriteAndReadGrowableObjects() {
+        for (int i = 0; i < NUM_VALUES; i++) {
+            GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, i);
+            fill(gdb, false, true);
+            assertFalse(gdb.hasValues());
+            assertTrue(gdb.hasObjects());
+            test(gdb, false, true);
+            assertFalse(gdb.hasValues());
+            assertFalse(gdb.hasObjects());
+        }
+    }
+
+    @Test public void testWriteAndMultipleReads() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, true, true);
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        gdb.save();
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, true, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+        gdb.restore();
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, true, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testPeekValues() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, true, false);
+        assertTrue(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+        for (int i = 0; i < gdb.writeValuePosition(); i++) {
+            gdb.peekByte(i);
+        }
+        assertTrue(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testPeekObjects() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, false, true);
+        assertFalse(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        for (int i = 0; i < gdb.writeValuePosition(); i++) {
+            gdb.peekByte(i);
+        }
+        assertFalse(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+    }
+
+    @Test public void testAppend() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb, true, true);
+        GrowableDataBuffer gdb2 = GrowableDataBuffer.getBuffer(NUM_BYTES, NUM_VALUES);
+        fill(gdb2, true, true);
+        gdb.append(gdb2);
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, true, true);
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        test(gdb, true, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+
+    @Test public void testReset() {
+        GrowableDataBuffer gdb = GrowableDataBuffer.getBuffer(0, 0);
+        fill(gdb, true, true);
+        assertTrue(gdb.hasValues());
+        assertTrue(gdb.hasObjects());
+        int valcapacity = gdb.valueCapacity();
+        int objcapacity = gdb.objectCapacity();
+        for (int i = 0; i < 5; i++) {
+            gdb.reset();
+            assertFalse(gdb.hasValues());
+            assertFalse(gdb.hasObjects());
+            fill(gdb, true, true);
+            assertTrue(gdb.hasValues());
+            assertTrue(gdb.hasObjects());
+        }
+        assertTrue(gdb.valueCapacity() == valcapacity);
+        assertTrue(gdb.objectCapacity() == objcapacity);
+        test(gdb, true, true);
+        assertFalse(gdb.hasValues());
+        assertFalse(gdb.hasObjects());
+    }
+}