diff options
author | Sven Gothel <[email protected]> | 2013-08-10 09:14:19 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2013-08-10 09:14:19 +0200 |
commit | 6332e13b2f0aa9818d37802302f04c90a4fa4239 (patch) | |
tree | b615630b4a886270721f82636a323ec36dac341c /src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java | |
parent | 590d78dc2ff24ce80976a30e35a99c06ef6750b0 (diff) |
GLMediaPlayer: Add multithreaded decoding w/ textureCount > 2 where available EGL/FFMPeg. WIP!
Off-thread decoding:
If validated (impl) textureCount > 2, decoding happens on extra thread.
If decoding requires GL context, a shared context is created for decoding thread.
API Changes:
- initGLStream(..): Adds 'textureCount' as argument.
- TextureSequence.TexSeqEventListener.newFrameAvailable(..) exposes the new frame available
- TextureSequence.TextureFrame exposes the PTS (video)
Implementation:
- 'int validateTextureCount(int)': implementation decides whether textureCount can be > 2, i.e. off-thread decoding allowed,
default is NO w/ textureCount==2!
- 'boolean requiresOffthreadGLCtx()': implementation decides whether shared context is required for off-thread decoding
- 'syncFrame2Audio(TextureFrame frame)': implementation shall handle a/v sync, due to audio stream details (pts, buffered frames)
- FFMPEGMediaPlayer extends GLMediaPlayerImpl, no more EGLMediaPlayerImpl (redundant)
+++
- SyncedRingbuffer: Expose T[] array
+++
TODO:
- syncAV!
- test Android
Diffstat (limited to 'src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java')
-rw-r--r-- | src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java | 501 |
1 files changed, 385 insertions, 116 deletions
diff --git a/src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java b/src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java index 2ff91a3f6..bc297dc21 100644 --- a/src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java +++ b/src/jogl/classes/jogamp/opengl/util/av/GLMediaPlayerImpl.java @@ -30,13 +30,17 @@ package jogamp.opengl.util.av; import java.io.IOException; import java.net.URLConnection; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; +import javax.media.nativewindow.AbstractGraphicsDevice; import javax.media.opengl.GL; import javax.media.opengl.GL2; +import javax.media.opengl.GLContext; +import javax.media.opengl.GLDrawable; +import javax.media.opengl.GLDrawableFactory; import javax.media.opengl.GLES2; import javax.media.opengl.GLException; +import javax.media.opengl.GLProfile; import com.jogamp.opengl.util.av.GLMediaPlayer; import com.jogamp.opengl.util.texture.Texture; @@ -45,7 +49,7 @@ import com.jogamp.opengl.util.texture.TextureSequence; /** * After object creation an implementation may customize the behavior: * <ul> - * <li>{@link #setTextureCount(int)}</li> + * <li>{@link #setDesTextureCount(int)}</li> * <li>{@link #setTextureTarget(int)}</li> * <li>{@link EGLMediaPlayerImpl#setEGLTexImageAttribs(boolean, boolean)}.</li> * </ul> @@ -59,6 +63,7 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { protected static final String unknown = "unknown"; protected State state; + protected int textureCount; protected int textureTarget; protected int textureFormat; @@ -74,35 +79,38 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { protected volatile float playSpeed = 1.0f; - /** Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int width = 0; - /** Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int height = 0; - /** Video fps. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Video fps. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected float fps = 0; - /** Stream bps. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Stream bps. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int bps_stream = 0; - /** Video bps. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Video bps. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int bps_video = 0; - /** Audio bps. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Audio bps. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int bps_audio = 0; - /** In frames. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** In frames. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int totalFrames = 0; - /** In ms. Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** In ms. Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected int duration = 0; - /** Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected String acodec = unknown; - /** Shall be set by the {@link #initGLStreamImpl(GL, int[])} method implementation. */ + /** Shall be set by the {@link #initGLStreamImpl(GL)} method implementation. */ protected String vcodec = unknown; protected int frameNumber = 0; + protected int currentVideoPTS = 0; - protected TextureSequence.TextureFrame[] texFrames = null; - protected HashMap<Integer, TextureSequence.TextureFrame> texFrameMap = new HashMap<Integer, TextureSequence.TextureFrame>(); + protected SyncedRingbuffer<TextureFrame> videoFramesFree = null; + protected SyncedRingbuffer<TextureFrame> videoFramesDecoded = null; + protected volatile TextureFrame lastFrame = null; + private ArrayList<GLMediaEventListener> eventListeners = new ArrayList<GLMediaEventListener>(); protected GLMediaPlayerImpl() { - this.textureCount=3; + this.textureCount=0; this.textureTarget=GL.GL_TEXTURE_2D; this.textureFormat = GL.GL_RGBA; this.textureInternalFormat = GL.GL_RGBA; @@ -112,14 +120,14 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { } @Override - public void setTextureUnit(int u) { texUnit = u; } + public final void setTextureUnit(int u) { texUnit = u; } @Override - public int getTextureUnit() { return texUnit; } + public final int getTextureUnit() { return texUnit; } + + @Override + public final int getTextureTarget() { return textureTarget; } - protected final void setTextureCount(int textureCount) { - this.textureCount=textureCount; - } @Override public final int getTextureCount() { return textureCount; } @@ -134,29 +142,7 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { public final int[] getTextureMinMagFilter() { return texMinMagFilter; } public final void setTextureWrapST(int[] wrapST) { texWrapST[0] = wrapST[0]; texWrapST[1] = wrapST[1];} - public final int[] getTextureWrapST() { return texWrapST; } - - @Override - public final TextureSequence.TextureFrame getLastTexture() throws IllegalStateException { - if(State.Uninitialized == state) { - throw new IllegalStateException("Instance not initialized: "+this); - } - return getLastTextureImpl(); - } - protected abstract TextureSequence.TextureFrame getLastTextureImpl(); - - @Override - public final synchronized TextureSequence.TextureFrame getNextTexture(GL gl, boolean blocking) throws IllegalStateException { - if(State.Uninitialized == state) { - throw new IllegalStateException("Instance not initialized: "+this); - } - if(State.Playing == state) { - final TextureSequence.TextureFrame f = getNextTextureImpl(gl, blocking); - return f; - } - return getLastTextureImpl(); - } - protected abstract TextureSequence.TextureFrame getNextTextureImpl(GL gl, boolean blocking); + public final int[] getTextureWrapST() { return texWrapST; } @Override public String getRequiredExtensionsShaderStub() throws IllegalStateException { @@ -229,12 +215,15 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { protected abstract boolean setPlaySpeedImpl(float rate); public final State start() { - switch(state) { + switch( state ) { case Stopped: + /** fall-through intended */ case Paused: - if(startImpl()) { + if( startImpl() ) { + resumeFramePusher(); state = State.Playing; } + default: } if(DEBUG) { System.err.println("Start: "+toString()); } return state; @@ -242,7 +231,8 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { protected abstract boolean startImpl(); public final State pause() { - if(State.Playing == state && pauseImpl()) { + if( State.Playing == state && pauseImpl() ) { + pauseFramePusher(); state = State.Paused; } if(DEBUG) { System.err.println("Pause: "+toString()); } @@ -251,12 +241,15 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { protected abstract boolean pauseImpl(); public final State stop() { - switch(state) { + switch( state ) { case Playing: + /** fall-through intended */ case Paused: - if(stopImpl()) { + if( stopImpl() ) { + pauseFramePusher(); state = State.Stopped; } + default: } if(DEBUG) { System.err.println("Stop: "+toString()); } return state; @@ -265,61 +258,70 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { @Override public final int getCurrentPosition() { - if(State.Uninitialized != state) { + if( State.Uninitialized != state ) { return getCurrentPositionImpl(); } return 0; } protected abstract int getCurrentPositionImpl(); + @Override + public final int getVideoPTS() { return currentVideoPTS; } + + @Override + public final int getAudioPTS() { + if( State.Uninitialized != state ) { + return getAudioPTSImpl(); + } + return 0; + } + protected abstract int getAudioPTSImpl(); + public final int seek(int msec) { - final int cp; + final int pts1; switch(state) { case Stopped: case Playing: case Paused: - cp = seekImpl(msec); + pauseFramePusher(); + pts1 = seekImpl(msec); + currentVideoPTS=pts1; + resumeFramePusher(); break; default: - cp = 0; + pts1 = 0; } if(DEBUG) { System.err.println("Seek("+msec+"): "+toString()); } - return cp; + return pts1; } protected abstract int seekImpl(int msec); public final State getState() { return state; } @Override - public final State initGLStream(GL gl, URLConnection urlConn) throws IllegalStateException, GLException, IOException { + public final State initGLStream(GL gl, int reqTextureCount, URLConnection urlConn) throws IllegalStateException, GLException, IOException { if(State.Uninitialized != state) { throw new IllegalStateException("Instance not in state "+State.Uninitialized+", but "+state+", "+this); } this.urlConn = urlConn; if (this.urlConn != null) { try { - if(null != gl) { - if(null!=texFrames) { - // re-init .. - removeAllImageTextures(gl); - } else { - texFrames = new TextureSequence.TextureFrame[textureCount]; - } - final int[] tex = new int[textureCount]; - { - gl.glGenTextures(textureCount, tex, 0); - final int err = gl.glGetError(); - if( GL.GL_NO_ERROR != err ) { - throw new RuntimeException("TextureNames creation failed (num: "+textureCount+"): err "+toHexString(err)); - } + if( null != gl ) { + removeAllTextureFrames(gl); + textureCount = validateTextureCount(reqTextureCount); + if( textureCount < 2 ) { + throw new InternalError("Validated texture count < 2: "+textureCount); } - initGLStreamImpl(gl, tex); - - for(int i=0; i<textureCount; i++) { - final TextureSequence.TextureFrame tf = createTexImage(gl, i, tex); - texFrames[i] = tf; - texFrameMap.put(tex[i], tf); + initGLStreamImpl(gl); // also initializes width, height, .. etc + videoFramesFree = new SyncedRingbuffer<TextureFrame>(createTexFrames(gl, textureCount), true /* full */); + if( 2 < textureCount ) { + videoFramesDecoded = new SyncedRingbuffer<TextureFrame>(new TextureFrame[textureCount], false /* full */); + framePusher = new FramePusher(gl, requiresOffthreadGLCtx()); + framePusher.doStart(); + } else { + videoFramesDecoded = null; } + lastFrame = videoFramesFree.getBlocking(false /* clearRef */ ); } state = State.Stopped; return state; @@ -329,35 +331,42 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { } return state; } + /** + * Returns the validated number of textures to be handled. + * <p> + * Default is always 2 textures, last texture and the decoding texture. + * </p> + */ + protected int validateTextureCount(int desiredTextureCount) { + return 2; + } + protected boolean requiresOffthreadGLCtx() { return false; } - /** - * Implementation shall set the following set of data here - * @param gl TODO - * @param texNames TODO - * @see #width - * @see #height - * @see #fps - * @see #bps_stream - * @see #totalFrames - * @see #acodec - * @see #vcodec - */ - protected abstract void initGLStreamImpl(GL gl, int[] texNames) throws IOException; - - protected TextureSequence.TextureFrame createTexImage(GL gl, int idx, int[] tex) { - return new TextureSequence.TextureFrame( createTexImageImpl(gl, idx, tex, width, height, false) ); + private final TextureFrame[] createTexFrames(GL gl, final int count) { + final int[] texNames = new int[count]; + gl.glGenTextures(count, texNames, 0); + final int err = gl.glGetError(); + if( GL.GL_NO_ERROR != err ) { + throw new RuntimeException("TextureNames creation failed (num: "+count+"): err "+toHexString(err)); + } + final TextureFrame[] texFrames = new TextureFrame[count]; + for(int i=0; i<count; i++) { + texFrames[i] = createTexImage(gl, texNames[i]); + } + return texFrames; } + protected abstract TextureFrame createTexImage(GL gl, int texName); - protected Texture createTexImageImpl(GL gl, int idx, int[] tex, int tWidth, int tHeight, boolean mustFlipVertically) { - if( 0 > tex[idx] ) { - throw new RuntimeException("TextureName "+toHexString(tex[idx])+" invalid."); + protected final Texture createTexImageImpl(GL gl, int texName, int tWidth, int tHeight, boolean mustFlipVertically) { + if( 0 > texName ) { + throw new RuntimeException("TextureName "+toHexString(texName)+" invalid."); } gl.glActiveTexture(GL.GL_TEXTURE0+getTextureUnit()); - gl.glBindTexture(textureTarget, tex[idx]); + gl.glBindTexture(textureTarget, texName); { final int err = gl.glGetError(); if( GL.GL_NO_ERROR != err ) { - throw new RuntimeException("Couldn't bind textureName "+toHexString(tex[idx])+" to 2D target, err "+toHexString(err)); + throw new RuntimeException("Couldn't bind textureName "+toHexString(texName)+" to 2D target, err "+toHexString(err)); } } @@ -389,30 +398,297 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { gl.glTexParameteri(textureTarget, GL.GL_TEXTURE_WRAP_S, texWrapST[0]); gl.glTexParameteri(textureTarget, GL.GL_TEXTURE_WRAP_T, texWrapST[1]); - return com.jogamp.opengl.util.texture.TextureIO.newTexture(tex[idx], - textureTarget, + return com.jogamp.opengl.util.texture.TextureIO.newTexture( + texName, textureTarget, tWidth, tHeight, width, height, mustFlipVertically); } + + private final void removeAllTextureFrames(GL gl) { + if( null != videoFramesFree ) { + final TextureFrame[] texFrames = videoFramesFree.getArray(); + videoFramesFree = null; + videoFramesDecoded = null; + lastFrame = null; + for(int i=0; i<texFrames.length; i++) { + final TextureFrame frame = texFrames[i]; + if(null != frame) { + destroyTexFrame(gl, frame); + texFrames[i] = null; + } + } + } + textureCount=0; + } + protected void destroyTexFrame(GL gl, TextureFrame frame) { + frame.getTexture().destroy(gl); + } + + /** + * Implementation shall set the following set of data here + * @param gl TODO + * @see #width + * @see #height + * @see #fps + * @see #bps_stream + * @see #totalFrames + * @see #acodec + * @see #vcodec + */ + protected abstract void initGLStreamImpl(GL gl) throws IOException; - protected void destroyTexImage(GL gl, TextureSequence.TextureFrame imgTex) { - imgTex.getTexture().destroy(gl); + @Override + public final TextureFrame getLastTexture() throws IllegalStateException { + if(State.Uninitialized == state) { + throw new IllegalStateException("Instance not initialized: "+this); + } + return lastFrame; } - protected void removeAllImageTextures(GL gl) { - if(null != texFrames) { - for(int i=0; i<textureCount; i++) { - final TextureSequence.TextureFrame imgTex = texFrames[i]; - if(null != imgTex) { - destroyTexImage(gl, imgTex); - texFrames[i] = null; + @Override + public final synchronized TextureFrame getNextTexture(GL gl, boolean blocking) throws IllegalStateException { + if(State.Uninitialized == state) { + throw new IllegalStateException("Instance not initialized: "+this); + } + if(State.Playing == state) { + TextureFrame nextFrame = null; + boolean ok = true; + try { + if( 2 < textureCount ) { + nextFrame = videoFramesDecoded.getBlocking(false /* clearRef */ ); + } else { + nextFrame = videoFramesFree.getBlocking(false /* clearRef */ ); + if( getNextTextureImpl(gl, nextFrame, blocking) ) { + newFrameAvailable(nextFrame); + } else { + ok = false; + } + } + if( ok ) { + currentVideoPTS = nextFrame.getPTS(); + if( blocking ) { + syncFrame2Audio(nextFrame); + } + final TextureFrame _lastFrame = lastFrame; + lastFrame = nextFrame; + videoFramesFree.putBlocking(_lastFrame); + } + } catch (InterruptedException e) { + ok = false; + e.printStackTrace(); + } finally { + if( !ok && null != nextFrame ) { // put back + videoFramesFree.put(nextFrame); } } } - texFrameMap.clear(); + return lastFrame; } + protected abstract boolean getNextTextureImpl(GL gl, TextureFrame nextFrame, boolean blocking); + protected abstract void syncFrame2Audio(TextureFrame frame); + + private final void newFrameAvailable(TextureFrame frame) { + frameNumber++; + synchronized(eventListenersLock) { + for(Iterator<GLMediaEventListener> i = eventListeners.iterator(); i.hasNext(); ) { + i.next().newFrameAvailable(this, frame, System.currentTimeMillis()); + } + } + } + + class FramePusher extends Thread { + private volatile boolean isRunning = false; + private volatile boolean isActive = false; + + private volatile boolean shallPause = true; + private volatile boolean shallStop = false; + + private final GL gl; + private GLDrawable dummyDrawable = null; + private GLContext sharedGLCtx = null; + + FramePusher(GL gl, boolean createSharedCtx) { + setDaemon(true); + this.gl = createSharedCtx ? createSharedGL(gl) : gl; + } + + private GL createSharedGL(GL gl) { + final GLContext glCtx = gl.getContext(); + final boolean glCtxCurrent = glCtx.isCurrent(); + final GLProfile glp = gl.getGLProfile(); + final GLDrawableFactory factory = GLDrawableFactory.getFactory(glp); + final AbstractGraphicsDevice device = glCtx.getGLDrawable().getNativeSurface().getGraphicsConfiguration().getScreen().getDevice(); + dummyDrawable = factory.createDummyDrawable(device, true, glp); // own device! + dummyDrawable.setRealized(true); + sharedGLCtx = dummyDrawable.createContext(glCtx); + makeCurrent(sharedGLCtx); + if( glCtxCurrent ) { + makeCurrent(glCtx); + } else { + sharedGLCtx.release(); + } + return sharedGLCtx.getGL(); + } + private void makeCurrent(GLContext ctx) { + if( GLContext.CONTEXT_NOT_CURRENT >= ctx.makeCurrent() ) { + throw new GLException("Couldn't make ctx current: "+ctx); + } + } + + private void destroySharedGL() { + if( null != sharedGLCtx ) { + if( sharedGLCtx.isCreated() ) { + // Catch dispose GLExceptions by GLEventListener, just 'print' them + // so we can continue with the destruction. + try { + sharedGLCtx.destroy(); + } catch (GLException gle) { + gle.printStackTrace(); + } + } + sharedGLCtx = null; + } + if( null != dummyDrawable ) { + final AbstractGraphicsDevice device = dummyDrawable.getNativeSurface().getGraphicsConfiguration().getScreen().getDevice(); + dummyDrawable.setRealized(false); + dummyDrawable = null; + device.close(); + } + } + + public synchronized void doPause() { + if( isActive ) { + shallPause = true; + while( isActive ) { + try { + this.wait(); // wait until paused + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + public synchronized void doResume() { + if( isRunning && !isActive ) { + shallPause = false; + while( !isActive ) { + this.notify(); // wake-up pause-block + try { + this.wait(); // wait until resumed + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + public synchronized void doStart() { + start(); + while( !isRunning ) { + try { + this.wait(); // wait until started + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + public synchronized void doStop() { + if( isRunning ) { + shallStop = true; + while( isRunning ) { + this.notify(); // wake-up pause-block (opt) + try { + this.wait(); // wait until stopped + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + public boolean isRunning() { return isRunning; } + public boolean isActive() { return isActive; } + + public void run() { + setName(getName()+"-FramePusher_"+FramePusherInstanceId); + FramePusherInstanceId++; + + synchronized ( this ) { + if( null != sharedGLCtx ) { + makeCurrent( sharedGLCtx ); + } + isRunning = true; + this.notify(); // wake-up doStart() + } + + while( !shallStop ){ + if( shallPause ) { + synchronized ( this ) { + while( shallPause && !shallStop ) { + isActive = false; + this.notify(); // wake-up doPause() + try { + this.wait(); // wait until resumed + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + isActive = true; + this.notify(); // wake-up doResume() + } + } + + if( !shallStop ) { + TextureFrame nextFrame = null; + boolean ok = false; + try { + nextFrame = videoFramesFree.getBlocking(true /* clearRef */ ); + if( getNextTextureImpl(gl, nextFrame, true) ) { + gl.glFinish(); + videoFramesDecoded.putBlocking(nextFrame); + newFrameAvailable(nextFrame); + ok = true; + } + } catch (InterruptedException e) { + if( !shallStop && !shallPause ) { + e.printStackTrace(); // oops + shallPause = false; + shallStop = true; + } + } finally { + if( !ok && null != nextFrame ) { // put back + videoFramesFree.put(nextFrame); + } + } + } + } + destroySharedGL(); + synchronized ( this ) { + isRunning = false; + isActive = false; + this.notify(); // wake-up doStop() + } + } + } + static int FramePusherInstanceId = 0; + private FramePusher framePusher = null; + private final void pauseFramePusher() { + if( null != framePusher ) { + framePusher.doPause(); + } + } + private final void resumeFramePusher() { + if( null != framePusher ) { + framePusher.doResume(); + } + } + private final void destroyFramePusher() { + if( null != framePusher ) { + framePusher.doStop(); + framePusher = null; + } + } + protected final void updateAttributes(int width, int height, int bps_stream, int bps_video, int bps_audio, float fps, int totalFrames, int duration, String vcodec, String acodec) { @@ -458,19 +734,12 @@ public abstract class GLMediaPlayerImpl implements GLMediaPlayer { } } } - protected final void newFrameAvailable() { - frameNumber++; - synchronized(eventListenersLock) { - for(Iterator<GLMediaEventListener> i = eventListeners.iterator(); i.hasNext(); ) { - i.next().newFrameAvailable(this, System.currentTimeMillis()); - } - } - } @Override public final synchronized State destroy(GL gl) { + destroyFramePusher(); destroyImpl(gl); - removeAllImageTextures(gl); + removeAllTextureFrames(gl); state = State.Uninitialized; return state; } |