/** * Copyright (c) 2003-2009, Xith3D Project Group all rights reserved. * * Portions based on the Java3D interface, Copyright by Sun Microsystems. * Many thanks to the developers of Java3D and Sun Microsystems for their * innovation and design. * * 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 the 'Xith3D Project Group' 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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) A * RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE */ package org.xith3d.loop; import java.util.Vector; import org.jagatoo.util.timing.JavaTimer; import org.xith3d.base.Xith3DEnvironment; import org.xith3d.loop.opscheduler.Animator; import org.xith3d.loop.opscheduler.OperationScheduler; import org.xith3d.loop.opscheduler.impl.OperationSchedulerImpl; import org.xith3d.scenegraph.utils.LODWorkerThread; /** * This loop renders the scene in a separate thread. * * You can schedule operations to be done by the thread in the next loop iteration. * @see org.xith3d.loop.opscheduler.ScheduledOperation * * You can schedule animations to be done by the thread in the next loop iteration. * @see org.xith3d.loop.opscheduler.Animatable * * You can get the fps count from this loop and set the maximum FPS. * @see org.xith3d.loop.FPSListener * * @author Marvin Froehlich (aka Qudus) */ public class RenderLoop extends UpdatingThread implements GameTimeHost, Updater, RenderLoopController { /** * You can start the RenderLoop in the same Thread as the app itself or in * a separate one. * * @author Marvin Froehlich (aka Qudus) */ public enum RunMode { /** * Let the RenderLoop run in the same Thread. */ RUN_IN_SAME_THREAD, /** * Let the RenderLoop run in a separate Thread. */ RUN_IN_SEPARATE_THREAD, /** * Let the RenderLoop run in a separate Thread and wait for * nextIteration() invokation. * * @see RenderLoop#nextFrame() */ RUN_IN_SEPARATE_THREAD_AND_WAIT; } /** * Use this enum for the setStopOperation() method.<br> * It controls the RenderLoop's behavior when it gets stopped. * * @see #setStopOperation(StopOperation) * * @author Marvin Froehlich (aka Qudus) */ public enum StopOperation { /** * When the RenderLoop stops, it just stops and does nothing more.<br> * This is the default. */ DO_NOTHING, /** * When the RenderLoop stops, the exit() method is called.<br> * The exit() method calls System.gc() and System.exit( 0 ); */ EXIT, /** * When the RenderLoop stops, the destroy() method is called.<br> * The destroy() method calls the destroy() method on each registered * RenderEngine. * * @see RenderLoop#destroy() */ DESTROY, /** * When the RenderLoop stops, the destroy() method is called.<br> * The destroy() method calls the destroy() method on each registered * RenderEngine. * Then the exit() method is called, which calls System.gc() and System.exit( 0 ); * * @see RenderLoop#destroy() * @see RenderLoop#exit() */ DESTROY_AND_EXIT; } public static final int PAUSE_RENDERING = 2; private RunMode runMode = null; private boolean nextFrameAllowed = false; private float maxFPS; private float fps = 0.0f; private long fpsCalcInterval; private final Vector< FPSListener > fpsListeners; private final Vector< RenderLoopListener > rlListeners; private OperationScheduler opScheder; private Updater updater; private Xith3DEnvironment x3dEnv = null; private StopOperation stopOperation = null; private Thread thread = null; /** * @return the RunMode this RenderLoop is running in. * Returns <i>null</i>, if the RenderLoop hasn't been started. * * @see RunMode */ public RunMode getRunMode() { return ( runMode ); } /** * Sets the StopOperation to invoke when the RenderLoop stops. * * @see StopOperation * * @param operation */ public void setStopOperation( StopOperation operation ) { if ( operation == null ) throw new IllegalArgumentException( "StopOperation must not be null" ); this.stopOperation = operation; } private StopOperation getStopOperation( RunMode runMode ) { if ( stopOperation == null ) { if ( runMode == null ) return ( null ); if ( runMode == RunMode.RUN_IN_SEPARATE_THREAD ) return ( StopOperation.DESTROY_AND_EXIT ); if ( runMode == RunMode.RUN_IN_SEPARATE_THREAD_AND_WAIT ) return ( StopOperation.DESTROY ); /* if (runMode == RunMode.RUN_IN_SAME_THREAD)*/ return ( StopOperation.DESTROY_AND_EXIT ); } return ( stopOperation ); } /** * @see StopOperation * * @return the StopOperation to invoke when the RenderLoop stops. */ public StopOperation getStopOperation() { return ( getStopOperation( getRunMode() ) ); } /** * Sets the OperationScheduler to be used by this RenderLoop (can be null). * * @param opScheder */ public void setOperationScheduler( OperationScheduler opScheder ) { if ( this.opScheder == opScheder ) return; this.opScheder = opScheder; } /** * @return the OperationScheduler used by this RenderLoop. */ public final OperationScheduler getOperationScheduler() { return ( opScheder ); } /** * @return the Animator used by this RenderLoop */ public Animator getAnimator() { return ( opScheder ); /* if ( opScheder instanceof Animator ) return ( (Animator)opScheder ); else return ( null ); */ } /** * Adds a new FPSListener to this loop to be notified periodically. * * @param l the new FPSListener */ public void addFPSListener( FPSListener l ) { //synchronized (fpsListeners) { fpsListeners.add( l ); } if ( l instanceof ConsciousFPSListener ) { ( (ConsciousFPSListener)l ).setRenderLoop( this ); } } /** * Removes an FPSListener from this loop. * * @param l the FPSListener to be removed */ public void removeFPSListener( FPSListener l ) { //synchronized (fpsListeners) { fpsListeners.remove( l ); } if ( l instanceof ConsciousFPSListener ) { ( (ConsciousFPSListener)l ).setRenderLoop( null ); } } /** * Sets the {@link Updater}, that manages the Updatables. */ public void setUpdater( Updater updater ) { this.updater = updater; } /** * @return the {@link Updater}, that manages the Updatables. */ public final Updater getUpdater() { return ( updater ); } /** * {@inheritDoc} */ public final void addUpdatable( Updatable updatable ) { if ( updater == null ) { throw new NullPointerException( "No Updater present!" ); } updater.addUpdatable( updatable ); } /** * {@inheritDoc} */ public final void removeUpdatable( Updatable updatable ) { if ( updater == null ) { throw new NullPointerException( "No Updater present!" ); } updater.removeUpdatable( updatable ); } /** * Adds a new RenderLoopListener to this loop. * * @param l the new RenderLoopListener */ public void addRenderLoopListener( RenderLoopListener l ) { synchronized ( rlListeners ) { rlListeners.add( l ); } } /** * Removes a RenderLoopListener from this loop. * * @param l the RenderLoopListener to be removed */ public void removeRenderLoopListener( RenderLoopListener l ) { synchronized ( rlListeners ) { rlListeners.remove( l ); } } /** * Sets the {@link Xith3DEnvironment} to this RenderLoop to be updated * frame-by-frame. * * @param env the {@link Xith3DEnvironment} to be updated by this loop */ public void setXith3DEnvironment( Xith3DEnvironment env ) { this.x3dEnv = env; } /** * @return the {@link Xith3DEnvironment}, which is linked to this RenderLoop. */ public final Xith3DEnvironment getXith3DEnvironment() { return ( x3dEnv ); } /** * This method is executed by the RenderLoop when the thread has been started. */ protected void onRenderLoopStarted() { synchronized ( rlListeners ) { for ( int i = 0; i < rlListeners.size(); i++ ) { rlListeners.get( i ).onRenderLoopStarted( this ); } } } /** * This method is executed by the RenderLoop when the thread has been stopped. * * @param gameTime the current gameTime * @param timingMode * @param averageFPS the average FPS over the time the loop was running */ protected void onRenderLoopStopped( long gameTime, TimingMode timingMode, float averageFPS ) { synchronized ( rlListeners ) { for ( int i = 0; i < rlListeners.size(); i++ ) { rlListeners.get( i ).onRenderLoopStopped( this, getGameTime(), timingMode, averageFPS ); } } } /** * This method is invoked, when the RenderLoop stopped and the * StopOperation is set to DESTROY or DESTROY_AND_EXIT.<br> * <br> * It calls destroy() on any registered RenderEngine and * sets any MouseDevice registered to the InputManager to non-exclusive.<br> * <br> * <b>Don't invoke this method directly except in a super call. * Use end() instead.</b> * * @see StopOperation * @see #setStopOperation(StopOperation) * @see #end() */ protected void destroy() { LODWorkerThread.getInstance().shutDown(); if ( x3dEnv != null ) { x3dEnv.destroy(); } } /** * This method is invoked, when the RenderLoop stopped and the * StopOperation is set to DESTROY_AND_EXIT.<br> * <br> * <b>Don't invoke this method directly except in a super call. * Use end() instead.</b> * * @see #setStopOperation(StopOperation) * @see #end() */ protected void exit() { System.gc(); //SPIDA - for our version of xith we do not want a system exit ever called // System.exit( 0 ); } /** * This method is invoked, when the RenderLoop stopped and the * StopOperation is set to DESTROY_AND_EXIT.<br> * <br> * <b>Don't invoke this method directly except in a super call. * Use end() instead.</b> * * @see #setStopOperation(StopOperation) * @see #end() */ protected void destroyAndExit() { destroy(); exit(); } /** * {@inheritDoc} */ @Override public void end() { if ( getThread() == null ) { if ( getStopOperation() == null ) { destroyAndExit(); } else { switch ( getStopOperation() ) { case DO_NOTHING: break; case DESTROY: destroy(); break; case EXIT: exit(); break; case DESTROY_AND_EXIT: destroyAndExit(); break; } } } else { super.end(); } } /** * This event is fired by the RenderLoop when the pauseMode has been increased. * * @param gameTime the current gameTime * @param timingMode * @param pauseMode the current pause-mode */ protected void onRenderLoopPaused( long gameTime, TimingMode timingMode, int pauseMode ) { synchronized ( rlListeners ) { for ( int i = 0; i < rlListeners.size(); i++ ) { rlListeners.get( i ).onRenderLoopPaused( this, getGameTime(), timingMode, pauseMode ); } } } /** * This event is fired by the RenderLoop when the pauseMode has been released. * * @param gameTime the current gameTime * @param timingMode * @param pauseMode the current pause-mode */ protected void onRenderLoopResumed( long gameTime, TimingMode timingMode, int pauseMode ) { synchronized ( rlListeners ) { for ( int i = 0; i < rlListeners.size(); i++ ) { rlListeners.get( i ).onRenderLoopResumed( this, getGameTime(), timingMode, pauseMode ); } } } /** * Sets the maximum frames per second. */ public void setMaxFPS( float maxFPS ) { this.maxFPS = maxFPS; setMinIterationTime( (long)( 1000000000.0f / maxFPS ) ); } /** * @return the maximum frames per second. */ public float getMaxFPS() { return ( maxFPS ); } /** * @return the current average frames per second */ public float getFPS() { return ( fps ); } /** * @return the average frames per second over the total game-time. */ public float getTotalAverageFPS() { if ( getGameNanoTime() == 0L ) return ( -1f ); return ( (float)getIterationsCount() / ( (float)getGameNanoTime() / 1000000000L ) ); } /** * Sets the interval in which the FPS are recalculated * * @param micros the new calculation interval */ public void setFPSCalcInterval( long micros ) { this.fpsCalcInterval = micros; } /** * @return the interval in which the FPS are recalculated */ public long getFPSCalcInterval() { return ( fpsCalcInterval ); } /** * This method is called each loop iteration before the renderNextFrame() * methiod. * * @param gameTime the current game time * @param frameTime time needed to render the last frame * @param timingMode */ protected void prepareNextFrame( long gameTime, long frameTime, TimingMode timingMode ) { if ( x3dEnv != null ) { x3dEnv.updatePhysicsEngine( gameTime, frameTime, timingMode ); x3dEnv.updateInputSystem( gameTime, timingMode ); } final OperationScheduler opScheder = getOperationScheduler(); if ( opScheder != null ) { opScheder.update( gameTime, frameTime, timingMode ); } if ( ( updater != null ) && ( updater != opScheder ) ) { updater.update( gameTime, frameTime, timingMode ); } } /** * Just calls the render() method on the linked Xith3DEnvironment. * * @param gameTime the current game time * @param frameTime time needed to render one frame * @param timingMode */ protected void renderNextFrame( long gameTime, long frameTime, TimingMode timingMode ) { if ( x3dEnv != null ) { x3dEnv.render( getGameNanoTime(), getLastNanoFrameTime() ); } else { System.err.println( "No Xith3DEnvironment registered!" ); } } /** * This method is called each loop iteration. * It just updates all registered Keyboard- and MouseDevices and calls invokeRendering(long). * Override this method if you want something more to be done each iteration. * * @param gameTime the current game time * @param frameTime time needed to render one frame * @param timingMode */ protected void loopIteration( long gameTime, long frameTime, TimingMode timingMode ) { prepareNextFrame( gameTime, frameTime, timingMode ); if ( ( getPauseMode() & PAUSE_RENDERING ) == 0 ) { renderNextFrame( gameTime, frameTime, timingMode ); } } /** * This method is called by the render loop each counting interval * * @param fps the average frames count during the last interval */ protected void onFPSCountIntervalHit( float fps ) { // notify FPSListeners if ( !fpsListeners.isEmpty() ) { //synchronized (fpsListeners) { for ( int i = 0; i < fpsListeners.size(); i++ ) { fpsListeners.get( i ).onFPSCountIntervalHit( getFPS() ); } } } } private long framesCountStartTime = 0L; private long framesCountTimeDelta; private int frames; /** * Calculates FPS. * * @param gameTime the current game time * @param timingMode * * @return true, if the count interval has been hit */ private void calcFPS( long gameTime, TimingMode timingMode ) { frames++; framesCountTimeDelta = timingMode.getMicroSeconds( gameTime - framesCountStartTime ); if ( framesCountTimeDelta >= fpsCalcInterval ) { fps = ( (float)frames / ( framesCountTimeDelta / 1000000f ) ); framesCountStartTime = gameTime; frames = 0; onFPSCountIntervalHit( fps ); } } /** * {@inheritDoc} */ @Override public void update( long gameTime, long frameTime, TimingMode timingMode ) { loopIteration( gameTime, frameTime, timingMode ); } /** * {@inheritDoc} */ @Override protected long nextIteration( boolean force ) { calcFPS( getGameTime(), getTimingMode() ); return ( super.nextIteration( force ) ); } /** * {@inheritDoc} */ public long nextFrame() { if ( runMode == null ) { return ( nextIteration( true ) ); } else if ( runMode == RunMode.RUN_IN_SEPARATE_THREAD_AND_WAIT ) { nextFrameAllowed = true; return ( -1L ); } throw new IllegalStateException( "The RenderLoop is not in interactive mode." ); } /** * {@inheritDoc} */ @Override protected void loop() { if ( runMode == RunMode.RUN_IN_SEPARATE_THREAD_AND_WAIT ) { while ( !isStopping() ) { if ( nextFrameAllowed ) { nextFrameAllowed = false; nextIteration( false ); } else { try { Thread.sleep( getMinIterationTime() / 1000000L ); } catch ( InterruptedException e ) { } } } } else { while ( !isStopping() ) { nextIteration( false ); } } } /** * {@inheritDoc} */ @Override public Thread getThread() { return ( thread ); } /** * {@inheritDoc} */ @Override public void run() { onRenderLoopStarted(); setPauseMode( PAUSE_NONE ); super.run(); RunMode tmpRunMode = this.runMode; this.runMode = null; onRenderLoopStopped( getGameTime(), getTimingMode(), getTotalAverageFPS() ); switch ( getStopOperation( tmpRunMode ) ) { case DO_NOTHING: break; case DESTROY: destroy(); break; case EXIT: exit(); break; case DESTROY_AND_EXIT: destroyAndExit(); break; } } /** * Starts this RenderLoop. * * @param runMode the RunMode this RenderLoop will run in * @param timingMode the TimingMode to use */ public void begin( RunMode runMode, TimingMode timingMode ) { if ( !isRunning() ) { if ( x3dEnv != null ) { x3dEnv.checkRenderPreferences(); } setTimingMode( timingMode ); this.runMode = runMode; this.nextFrameAllowed = false; System.gc(); if ( ( runMode == RunMode.RUN_IN_SEPARATE_THREAD ) || ( runMode == RunMode.RUN_IN_SEPARATE_THREAD_AND_WAIT ) ) { this.thread = new Thread( this ); thread.setName( "Xith3D render thread" ); this.thread.start(); } else { this.thread = Thread.currentThread(); run(); } } } /** * Starts this RenderLoop. * * @param runMode the RunMode this RenderLoop will run in */ public final void begin( RunMode runMode ) { begin( runMode, TimingMode.MICROSECONDS ); } /** * Starts this rendering-loop in a separate Thread and with TimingMode.MILLISECONDS. * * @param timingMode the TimingMode to use */ public final void begin( TimingMode timingMode ) { begin( RunMode.RUN_IN_SAME_THREAD, timingMode ); } /** * Starts this rendering-loop in a separate Thread and with TimingMode.MILLISECONDS. */ public final void begin() { begin( RunMode.RUN_IN_SAME_THREAD, TimingMode.MICROSECONDS ); } /** * pauses rendering in this RenderLoop */ public void pauseRendering() { setPauseMode( getPauseMode() + RenderLoop.PAUSE_RENDERING ); onRenderLoopPaused( getGameTime(), getTimingMode(), getPauseMode() ); } /** * resumes rendering in this RenderLoop */ public void resumeRendering() { setPauseMode( getPauseMode() - RenderLoop.PAUSE_RENDERING ); onRenderLoopResumed( getGameTime(), getTimingMode(), getPauseMode() ); } /** * Creates a new instance * * @param x3dEnv the {@link Xith3DEnvironment} to be linked with this RenderLoop. * @param maxFPS the maximum FPS to render at */ public RenderLoop( Xith3DEnvironment x3dEnv, float maxFPS ) { this.setTimer( new JavaTimer() ); this.fpsListeners = new Vector< FPSListener >(); this.rlListeners = new Vector< RenderLoopListener >(); this.setXith3DEnvironment( x3dEnv ); this.setFPSCalcInterval( 500000L ); this.setMaxFPS( maxFPS ); OperationSchedulerImpl opScheder = new OperationSchedulerImpl( this ); setOperationScheduler( opScheder ); setUpdater( opScheder ); } /** * Creates a new instance * * @param maxFPS the maximum FPS to render at */ public RenderLoop( float maxFPS ) { this( null, maxFPS ); } /** * Creates a new instance * * @param x3dEnv the {@link Xith3DEnvironment} to be linked with this RenderLoop. */ public RenderLoop( Xith3DEnvironment x3dEnv ) { this( x3dEnv, Float.MAX_VALUE ); } /** * Creates a new instance */ public RenderLoop() { this( Float.MAX_VALUE ); } }