/* * Copyright (c) 2016, Metron, 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: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions 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 Metron, Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.metsci.glimpse.gl; import static com.jogamp.common.nio.Buffers.*; import static com.metsci.glimpse.gl.util.GLUtils.*; import static java.lang.Math.*; import static javax.media.opengl.GL.*; import java.nio.ByteBuffer; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import javax.media.opengl.GL; /** * Represents a device buffer that needs to be re-written frequently, and * frequently read for rendering as well. Helpful for migrating away from * immediate-mode rendering. * <p> * Uses the recommended approach for * <a href="https://www.opengl.org/wiki/Buffer_Object_Streaming">buffer object streaming</a>. * Specifically: * <ol> * <li>Allocate a large block of device-buffer memory * <li>Write to a small section of the block * <li>Render using the written section * <li>Write to the <i>next</i> section of the block * <li>Render using the newly written section * <li>Repeat * <li>Once the whole block has been used, allocate a new block * </ol> * When a new block is allocated, the old block is dereferenced, but not * actually deleted until pending render operations are done with it. This * approach avoids driver-level synchronization, because the new block can * be written to without waiting for pending render operations to finish. * <p> * The cost is an increase in device-memory allocations. However, allocating * in large blocks keeps the frequency of allocations down. Furthermore, in * most cases, successive blocks will be the same size, so it is reasonable * to assume that old blocks will be recycled by the driver's memory manager. * For more info see the explanation <a href="https://www.opengl.org/discussion_boards/showthread.php/170118-VBOs-strangely-slow?p=1197780#post1197780">here</a>. * <p> * Expected usage looks something like this: * <pre> * // At init-time * * GLStreamingBuffer xyVbo = new GLStreamingBuffer( GL_ARRAY_BUFFER, GL_STREAM_DRAW, 100 ); * ... * * // At render-time * * int maxVertices = 10000; * int floatsPerVertex = 2; * FloatBuffer xyBuffer = xyVbo.mapFloats( gl, floatsPerVertex * maxVertices ); * * xyBuffer.put( x0 ).put( y0 ); * xyBuffer.put( x1 ).put( y1 ); * ... * * int numVertices = xyBuffer.position( ) / floatsPerVertex; * xyVbo.seal( gl ); * * gl.glBindBuffer( xyVbo.target, xyVbo.buffer( ) ); * gl.glVertexAttribPointer( ..., xyVbo.sealedOffset( ) ); * gl.glDrawArrays( ..., 0, numVertices ); * </pre> */ public class GLStreamingBuffer { /** * Passed to {@link GL#glBufferData(int, long, java.nio.Buffer, int)} * when allocating buffer space */ public final int usage; /** * How many times larger than the mapped size to allocate, when * we have to allocate new space for the buffer -- larger factors * result in less frequent reallocs, but higher memory use */ protected final int blockSizeFactor; /** * Zero until the first call to {@link #buffer(GL)} */ protected int buffer; /** * The byte size of the space currently allocated for the buffer */ protected long blockSize; /** * The byte offset of the most recently sealed range, or -1 if * no ranges have been sealed yet */ protected long sealedOffset; /** * When mapped: the byte offset of the mapped range * When sealed: the byte offset of the next range to be mapped */ protected long mappedOffset; /** * When mapped: the byte size of the mapped range * When sealed: zero */ protected long mappedSize; public GLStreamingBuffer( int usage, int blockSizeFactor ) { this.usage = usage; this.blockSizeFactor = blockSizeFactor; this.buffer = 0; this.blockSize = 0; this.sealedOffset = -1; this.mappedOffset = 0; this.mappedSize = 0; } /** * Returns the buffer handle, as created by e.g. {@link GL#glGenBuffers(int, java.nio.IntBuffer)}. */ public int buffer( GL gl ) { if ( this.buffer == 0 ) { this.buffer = genBuffer( gl ); } return this.buffer; } /** * Returns the offset into {@link #buffer()} of the most recently sealed range -- e.g. for use * with {@link javax.media.opengl.GL2ES2#glVertexAttribPointer(int, int, int, boolean, int, long)}. * <p> * Returns -1 if {@link #seal(GL)} has not been called yet. */ public long sealedOffset( ) { return this.sealedOffset; } /** * Convenience method that maps a region, copies data into it using {@link FloatBuffer#put(FloatBuffer)}, * and then seals the region. * <p> * Note that this will modify the buffer's {@code position}. */ public void setFloats( GL gl, FloatBuffer floats ) { FloatBuffer mapped = this.mapFloats( gl, floats.remaining( ) ); mapped.put( floats ); this.seal( gl ); } /** * Convenience method that maps a region, copies data into it using {@link DoubleBuffer#put(DoubleBuffer)}, * and then seals the region. * <p> * Note that this will modify the buffer's {@code position}. */ public void setDoubles( GL gl, DoubleBuffer doubles ) { DoubleBuffer mapped = this.mapDoubles( gl, doubles.remaining( ) ); mapped.put( doubles ); this.seal( gl ); } /** * Convenience method that maps a region, copies data into it using {@link IntBuffer#put(IntBuffer)}, * and then seals the region. * <p> * Note that this will modify the buffer's {@code position}. */ public void setInts( GL gl, IntBuffer ints ) { IntBuffer mapped = this.mapInts( gl, ints.remaining( ) ); mapped.put( ints ); this.seal( gl ); } /** * Convenience method that maps a region, copies data into it using {@link ByteBuffer#put(ByteBuffer)}, * and then seals the region. * <p> * Note that this will modify the buffer's {@code position}. */ public void setBytes( GL gl, ByteBuffer bytes ) { ByteBuffer mapped = this.mapBytes( gl, bytes.remaining( ) ); mapped.put( bytes ); this.seal( gl ); } /** * Convenience wrapper around {@link #mapBytes(GL, long)} -- converts {@code numFloats} to a * byte count, and converts the returned buffer to a {@link FloatBuffer}. */ public FloatBuffer mapFloats( GL gl, long numFloats ) { return this.mapBytes( gl, numFloats * SIZEOF_FLOAT ).asFloatBuffer( ); } /** * Convenience wrapper around {@link #mapBytes(GL, long)} -- converts {@code numDoubles} to a * byte count, and converts the returned buffer to a {@link DoubleBuffer}. */ public DoubleBuffer mapDoubles( GL gl, long numDoubles ) { return this.mapBytes( gl, numDoubles * SIZEOF_DOUBLE ).asDoubleBuffer( ); } /** * Convenience wrapper around {@link #mapBytes(GL, long)} -- converts {@code numInts} to a * byte count, and converts the returned buffer to an {@link IntBuffer}. */ public IntBuffer mapInts( GL gl, long numInts ) { return this.mapBytes( gl, numInts * SIZEOF_INT ).asIntBuffer( ); } /** * Returns a buffer representing host memory owned by the graphics driver. The returned buffer * should be treated as <em>write-only</em>. After writing to the buffer, call {@link #seal(GL)} * to indicate to the driver that the newly written contents are ready to be pushed to the device. * <p> * It is okay to request more bytes than will actually be written, as long as you then avoid * actually trying to use the values in the unwritten region (e.g. by passing the appropriate * number of vertices to {@link GL#glDrawArrays(int, int, int)}. * <p> * Note, however, that the driver may push the entire mapped region to the device -- so while * {@code numBytes} is an upper bound, it should be a reasonably tight upper bound. */ public ByteBuffer mapBytes( GL gl, long numBytes ) { if ( this.mappedSize != 0 ) { throw new RuntimeException( "Buffer is already mapped -- must be sealed before being mapped again" ); } gl.glBindBuffer( GL_ARRAY_BUFFER, this.buffer( gl ) ); // Seems recommended to map in multiples of 64 ... I guess for alignment reasons? this.mappedSize = nextMultiple( numBytes, 64 ); if ( this.mappedOffset + this.mappedSize > this.blockSize ) { // Allocate a block large enough that we don't have to allocate too frequently this.blockSize = max( this.blockSize, this.blockSizeFactor * this.mappedSize ); // Allocate new space, and orphan the old space gl.glBufferData( GL_ARRAY_BUFFER, this.blockSize, null, this.usage ); // Start at the beginning of the new space this.mappedOffset = 0; } return gl.glMapBufferRange( GL_ARRAY_BUFFER, this.mappedOffset, this.mappedSize, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT ); } /** * Returns the smallest multiple of b that is greater than or equal to a. */ protected static long nextMultiple( long a, long b ) { return ( b * ( ( ( a - 1 ) / b ) + 1 ) ); } /** * Unmaps the currently mapped range. After this, the sealed range can be read by GL calls. */ public void seal( GL gl ) { if ( this.mappedSize == 0 ) { throw new RuntimeException( "Buffer is not currently mapped" ); } gl.glBindBuffer( GL_ARRAY_BUFFER, this.buffer ); gl.glUnmapBuffer( GL_ARRAY_BUFFER ); this.sealedOffset = this.mappedOffset; this.mappedOffset += this.mappedSize; this.mappedSize = 0; } /** * Deletes the buffer (unmapping first, if necessary), and resets this object to the way * it was before {@link #mapBytes(GL, long)} was first called. * <p> * This object can be safely reused after being disposed, but in most cases there is no * significant advantage to doing so. */ public void dispose( GL gl ) { if ( this.mappedSize != 0 ) { gl.glBindBuffer( GL_ARRAY_BUFFER, this.buffer ); gl.glUnmapBuffer( GL_ARRAY_BUFFER ); this.mappedSize = 0; } if ( this.buffer != 0 ) { deleteBuffers( gl, this.buffer ); this.buffer = 0; } this.blockSize = 0; this.sealedOffset = -1; this.mappedOffset = 0; } }