/*
 * Copyright (c) 2002-2011 LWJGL Project
 * 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 'LWJGL' 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) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lwjgl.test.opengl.sprites;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.*;
import org.lwjgl.util.mapped.MappedObject;
import org.lwjgl.util.mapped.MappedObjectClassLoader;
import org.lwjgl.util.mapped.MappedObjectTransformer;
import org.lwjgl.util.mapped.MappedType;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Random;
import javax.imageio.ImageIO;

import static org.lwjgl.opengl.EXTTransformFeedback.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;

/**
 * Sprite rendering demo. Three implementations are supported:
 * a) CPU animation + BufferData VBO update.
 * b) CPU animation + MapBufferRange VBO update.
 * c) GPU animation using transform feedback with a vertex shader.
 *
 * @author Spasi
 * @since 18/3/2011
 */
public final class SpriteShootoutMapped {

	static final int SCREEN_WIDTH  = 800;
	static final int SCREEN_HEIGHT = 600;

	private static final int ANIMATION_TICKS = 60;

	private boolean run       = true;
	private boolean render    = true;
	private boolean colorMask = true;
	private boolean animate   = true;
	private boolean smooth;
	private boolean vsync;

	int ballSize  = 42;
	int ballCount = 100 * 1000;

	private SpriteRenderer renderer;

	// OpenGL stuff
	private int texID;
	private int texBigID;
	private int texSmallID;

	long animateTime;

	private SpriteShootoutMapped() {
	}

	public static void main(String[] args) {
		MappedObjectTransformer.register(Pixel4b.class);
		MappedObjectTransformer.register(Pixel3b.class);
		MappedObjectTransformer.register(Sprite.class);
		MappedObjectTransformer.register(SpriteRender.class);

		if ( MappedObjectClassLoader.fork(SpriteShootoutMapped.class, args) )
			return;

		try {
			new SpriteShootoutMapped().start();
		} catch (LWJGLException e) {
			e.printStackTrace();
		}
	}

	private void start() throws LWJGLException {
		try {
			initGL();

			final ContextCapabilities caps = GLContext.getCapabilities();
			if ( !true && (caps.OpenGL30 || caps.GL_EXT_transform_feedback) )
				renderer = new SpriteRendererTF();
			else if ( true && caps.GL_ARB_map_buffer_range )
				renderer = new SpriteRendererMapped();
			else
				renderer = new SpriteRendererPlain();

			updateBalls(ballCount);
			run();
		} catch (Throwable t) {
			t.printStackTrace();
		} finally {
			destroy();
		}
	}

	private void initGL() throws LWJGLException {
		Display.setLocation((Display.getDisplayMode().getWidth() - SCREEN_WIDTH) / 2,
		                    (Display.getDisplayMode().getHeight() - SCREEN_HEIGHT) / 2);
		Display.setDisplayMode(new DisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT));
		Display.setTitle("Sprite Shootout");
		Display.create();
		//Display.create(new PixelFormat(), new ContextAttribs(4, 1).withProfileCompatibility(true).withDebug(true));
		//AMDDebugOutput.glDebugMessageCallbackAMD(new AMDDebugOutputCallback());

		if ( !GLContext.getCapabilities().OpenGL20 )
			throw new RuntimeException("OpenGL 2.0 is required for this demo.");

		// Setup viewport

		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(0, SCREEN_WIDTH, 0, SCREEN_HEIGHT, -1.0, 1.0);

		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

		glClearColor(1.0f, 1.0f, 1.0f, 0.0f);

		// Create textures

		try {
			texSmallID = createTexture("res/ball_sm.png");
			texBigID = createTexture("res/ball.png");
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(-1);
		}
		texID = texBigID;

		// Setup rendering state

		glEnable(GL_BLEND);
		glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

		glEnable(GL_ALPHA_TEST);
		glAlphaFunc(GL_GREATER, 0.0f);

		glColorMask(colorMask, colorMask, colorMask, false);
		glDepthMask(false);
		glDisable(GL_DEPTH_TEST);

		// Setup geometry

		glEnableClientState(GL_VERTEX_ARRAY);

		Util.checkGLError();
	}

	private static int createTexture(final String path) throws IOException {
		final BufferedImage img = ImageIO.read(SpriteShootoutMapped.class.getClassLoader().getResource(path));

		final int w = img.getWidth();
		final int h = img.getHeight();

		final ByteBuffer buffer = readImage(img);

		final int texID = glGenTextures();

		glBindTexture(GL_TEXTURE_2D, texID);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, buffer);

		return texID;
	}

	public static class Pixel4b extends MappedObject {

		public byte r, g, b, a;

	}

	@MappedType(align = 3)
	public static class Pixel3b extends MappedObject {

		public byte r, g, b;

	}

	private static ByteBuffer readImage(final BufferedImage img) throws IOException {
		final Raster raster = img.getRaster();

		final int bands = raster.getNumBands();

		final int w = img.getWidth();
		final int h = img.getHeight();

		final int count = w * h;

		final byte[] pixels = new byte[count * bands];
		raster.getDataElements(0, 0, w, h, pixels);

		if ( bands == 4 ) {
			Pixel4b p = Pixel4b.malloc(count);

			int b = 0;
			for ( int i = 0; i < count; i++, b += 4 ) {
				// Pre-multiply alpha
				final float a = unpackUByte01(pixels[b + 3]);

				p.view = i;
				p.r = packUByte01(unpackUByte01(pixels[b + 2]) * a);
				p.g = packUByte01(unpackUByte01(pixels[b + 1]) * a);
				p.b = packUByte01(unpackUByte01(pixels[b + 0]) * a);
				p.a = pixels[b + 3];
			}

			return p.backingByteBuffer();
		} else if ( bands == 3 ) {
			Pixel3b p = Pixel3b.malloc(count);

			int b = 0;
			for ( int i = 0; i < count; i++, b += 3 ) {
				p.view = i;
				p.r = pixels[b + 2];
				p.g = pixels[b + 1];
				p.b = pixels[b + 0];
			}

			return p.backingByteBuffer();
		} else {
			ByteBuffer p = BufferUtils.createByteBuffer(count * bands);
			p.put(pixels, 0, p.capacity());
			p.flip();
			return p;
		}

	}

	private static float unpackUByte01(final byte x) {
		return (x & 0xFF) / 255.0f;
	}

	private static byte packUByte01(final float x) {
		return (byte)(x * 255.0f);
	}

	private void updateBalls(final int count) {
		System.out.println("NUMBER OF BALLS: " + count);
		renderer.updateBalls(ballCount);
	}

	private void run() throws LWJGLException {
		long startTime = System.currentTimeMillis() + 5000;
		long fps = 0;

		long time = Sys.getTime();
		final int ticksPerUpdate = (int)(Sys.getTimerResolution() / ANIMATION_TICKS);

		renderer.render(false, true, 0);

		while ( run ) {
			Display.processMessages();
			handleInput();

			glClear(GL_COLOR_BUFFER_BIT);

			final long currTime = Sys.getTime();
			final int delta = (int)(currTime - time);
			if ( smooth || delta >= ticksPerUpdate ) {
				renderer.render(render, animate, delta);
				time = currTime;
			} else
				renderer.render(render, false, 0);

			Display.update(false);
			//Display.sync(60);

			if ( startTime > System.currentTimeMillis() ) {
				fps++;
			} else {
				long timeUsed = 5000 + (startTime - System.currentTimeMillis());
				startTime = System.currentTimeMillis() + 5000;
				System.out.println("FPS: " + (Math.round(fps / (timeUsed / 1000.0) * 10) / 10.0) + ", Balls: " + ballCount);
				System.out.println("Animation: " + animateTime / fps);
				animateTime = 0;
				fps = 0;
			}
		}
	}

	private void handleInput() throws LWJGLException {
		if ( Display.isCloseRequested() )
			run = false;

		while ( Keyboard.next() ) {
			if ( Keyboard.getEventKeyState() )
				continue;

			switch ( Keyboard.getEventKey() ) {
				case Keyboard.KEY_1:
				case Keyboard.KEY_2:
				case Keyboard.KEY_3:
				case Keyboard.KEY_4:
				case Keyboard.KEY_5:
				case Keyboard.KEY_6:
				case Keyboard.KEY_7:
				case Keyboard.KEY_8:
				case Keyboard.KEY_9:
				case Keyboard.KEY_0:
					ballCount = 1 << (Keyboard.getEventKey() - Keyboard.KEY_1);
					updateBalls(ballCount);
					break;
				case Keyboard.KEY_ADD:
				case Keyboard.KEY_SUBTRACT:
					int mult;
					if ( Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT) )
						mult = 1000;
					else if ( Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU) )
						mult = 100;
					else if ( Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) )
						mult = 10;
					else
						mult = 1;
					if ( Keyboard.getEventKey() == Keyboard.KEY_SUBTRACT )
						mult = -mult;
					ballCount += mult * 100;
					if ( ballCount <= 0 )
						ballCount = 1;
					updateBalls(ballCount);
					break;
				case Keyboard.KEY_ESCAPE:
					run = false;
					break;
				case Keyboard.KEY_A:
					animate = !animate;
					System.out.println("Animation is now " + (animate ? "on" : "off") + ".");
					break;
				case Keyboard.KEY_C:
					colorMask = !colorMask;
					glColorMask(colorMask, colorMask, colorMask, false);
					System.out.println("Color mask is now " + (colorMask ? "on" : "off") + ".");
					// Disable alpha test when color mask is off, else we get no benefit.
					if ( colorMask ) {
						glEnable(GL_BLEND);
						glEnable(GL_ALPHA_TEST);
					} else {
						glDisable(GL_BLEND);
						glDisable(GL_ALPHA_TEST);
					}
					break;
				case Keyboard.KEY_R:
					render = !render;
					System.out.println("Rendering is now " + (render ? "on" : "off") + ".");
					break;
				case Keyboard.KEY_S:
					smooth = !smooth;
					System.out.println("Smooth animation is now " + (smooth ? "on" : "off") + ".");
					break;
				case Keyboard.KEY_T:
					if ( texID == texBigID ) {
						texID = texSmallID;
						ballSize = 16;
					} else {
						texID = texBigID;
						ballSize = 42;
					}
					renderer.updateBallSize();
					glBindTexture(GL_TEXTURE_2D, texID);
					System.out.println("Now using the " + (texID == texBigID ? "big" : "small") + " texture.");
					break;
				case Keyboard.KEY_V:
					vsync = !vsync;
					Display.setVSyncEnabled(vsync);
					System.out.println("VSYNC is now " + (vsync ? "enabled" : "disabled") + ".");
					break;
			}
		}

		while ( Mouse.next() ) ;
	}

	private void destroy() {
		Display.destroy();
	}

	public static class Sprite extends MappedObject {

		public float dx, x;
		public float dy, y;

	}

	public static class SpriteRender extends MappedObject {

		public float x, y;

	}

	private abstract class SpriteRenderer {

		protected Sprite sprites;

		protected int spriteCount;

		protected int vshID;
		protected int progID;

		protected void createProgram() {
			final int fshID = glCreateShader(GL_FRAGMENT_SHADER);
			glShaderSource(fshID, "uniform sampler2D COLOR_MAP;\n" +
			                      "void main(void) {\n" +
			                      "     gl_FragColor = texture2D(COLOR_MAP, gl_PointCoord);\n" +
			                      "}");
			glCompileShader(fshID);
			if ( glGetShaderi(fshID, GL_COMPILE_STATUS) == GL_FALSE ) {
				System.out.println(glGetShaderInfoLog(fshID, glGetShaderi(fshID, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to compile fragment shader.");
			}

			progID = glCreateProgram();
			glAttachShader(progID, vshID);
			glAttachShader(progID, fshID);
			glLinkProgram(progID);
			if ( glGetProgrami(progID, GL_LINK_STATUS) == GL_FALSE ) {
				System.out.println(glGetProgramInfoLog(progID, glGetProgrami(progID, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to link shader program.");
			}

			glUseProgram(progID);
			glUniform1i(glGetUniformLocation(progID, "COLOR_MAP"), 0);

			updateBallSize();

			glEnableClientState(GL_VERTEX_ARRAY);
		}

		public void updateBallSize() {
			glPointSize(ballSize);
		}

		public abstract void updateBalls(int count);

		protected abstract void render(boolean render, boolean animate, int delta);

	}

	private abstract class SpriteRendererBatched extends SpriteRenderer {

		protected static final int BALLS_PER_BATCH = 10 * 1000;

		SpriteRendererBatched() {
			vshID = glCreateShader(GL_VERTEX_SHADER);
			glShaderSource(vshID, "void main(void) {\n" +
			                      "     gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n" +
			                      "}");
			glCompileShader(vshID);
			if ( glGetShaderi(vshID, GL_COMPILE_STATUS) == GL_FALSE ) {
				System.out.println(glGetShaderInfoLog(vshID, glGetShaderi(vshID, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to compile vertex shader.");
			}

			createProgram();
		}

		public void updateBalls(final int count) {
			final Random random = new Random();

			final Sprite newSprites = Sprite.malloc(count);
			if ( sprites != null ) {
				sprites.view = 0;
				sprites.copyRange(newSprites, Math.min(count, spriteCount));
			}

			if ( count > spriteCount ) {
				for ( int i = spriteCount; i < count; i++ ) {
					newSprites.view = i;

					newSprites.x = (int)(random.nextFloat() * (SCREEN_WIDTH - ballSize) + ballSize * 0.5f);
					newSprites.y = (int)(random.nextFloat() * (SCREEN_HEIGHT - ballSize) + ballSize * 0.5f);
					newSprites.dx = random.nextFloat() * 0.4f - 0.2f;
					newSprites.dy = random.nextFloat() * 0.4f - 0.2f;
				}
			}

			sprites = newSprites;
			spriteCount = count;
		}

		protected void animate(
			final Sprite sprite,
			final SpriteRender spriteRender,
			final int ballSize, final int ballIndex, final int batchSize, final int delta
		) {
			final float ballRadius = ballSize * 0.5f;
			final float boundW = SCREEN_WIDTH - ballRadius;
			final float boundH = SCREEN_HEIGHT - ballRadius;

			final Sprite[] sprites = sprite.asArray();
			final SpriteRender[] spritesRender = spriteRender.asArray();
			for ( int b = ballIndex, r = 0, len = (ballIndex + batchSize); b < len; b++, r++ ) {
				float dx = sprites[b].dx;
				float x = sprites[b].x;

				x += dx * delta;
				if ( x < ballRadius ) {
					x = ballRadius;
					dx = -dx;
				} else if ( x > boundW ) {
					x = boundW;
					dx = -dx;
				}

				sprites[b].dx = dx;
				sprites[b].x = x;
				spritesRender[r].x = x;

				float dy = sprites[b].dy;
				float y = sprites[b].y;

				y += dy * delta;
				if ( y < ballRadius ) {
					y = ballRadius;
					dy = -dy;
				} else if ( y > boundH ) {
					y = boundH;
					dy = -dy;
				}

				sprites[b].dy = dy;
				sprites[b].y = y;
				spritesRender[r].y = y;
			}
		}

	}

	private class SpriteRendererPlain extends SpriteRendererBatched {

		private final int DATA_PER_BATCH = BALLS_PER_BATCH * 2 * 4; // balls * 2 floats * 4 bytes

		protected int[] animVBO;

		private SpriteRender spritesRender;

		SpriteRendererPlain() {
			System.out.println("Shootout Implementation: CPU animation & BufferData");
			spritesRender = SpriteRender.malloc(BALLS_PER_BATCH);
		}

		public void updateBalls(final int count) {
			super.updateBalls(count);

			final int batchCount = count / BALLS_PER_BATCH + (count % BALLS_PER_BATCH == 0 ? 0 : 1);
			if ( animVBO != null && batchCount == animVBO.length )
				return;

			final int[] newAnimVBO = new int[batchCount];
			if ( animVBO != null ) {
				System.arraycopy(animVBO, 0, newAnimVBO, 0, Math.min(animVBO.length, newAnimVBO.length));
				for ( int i = newAnimVBO.length; i < animVBO.length; i++ )
					glDeleteBuffers(animVBO[i]);
			}
			for ( int i = animVBO == null ? 0 : animVBO.length; i < newAnimVBO.length; i++ ) {
				newAnimVBO[i] = glGenBuffers();
				glBindBuffer(GL_ARRAY_BUFFER, newAnimVBO[i]);
			}

			animVBO = newAnimVBO;
		}

		public void render(final boolean render, final boolean animate, final int delta) {
			int batchSize = Math.min(ballCount, BALLS_PER_BATCH);
			int ballIndex = 0;
			int batchIndex = 0;
			while ( ballIndex < ballCount ) {
				glBindBuffer(GL_ARRAY_BUFFER, animVBO[batchIndex]);

				if ( animate )
					animate(ballIndex, batchSize, delta);

				if ( render ) {
					glVertexPointer(2, GL_FLOAT, 0, 0);
					glDrawArrays(GL_POINTS, 0, batchSize);
				}

				ballIndex += batchSize;
				batchSize = Math.min(ballCount - ballIndex, BALLS_PER_BATCH);
				batchIndex++;
			}
		}

		private void animate(final int ballIndex, final int batchSize, final int delta) {
			animate(
				sprites, spritesRender,
				ballSize, ballIndex, batchSize, delta
			);

			glBufferData(GL_ARRAY_BUFFER, DATA_PER_BATCH, GL_STREAM_DRAW);
			glBufferSubData(GL_ARRAY_BUFFER, 0, spritesRender.backingByteBuffer());
		}

	}

	private class SpriteRendererMapped extends SpriteRendererBatched {

		private StreamVBO animVBO;

		SpriteRendererMapped() {
			System.out.println("Shootout Implementation: CPU animation & MapBufferRange");
		}

		public void updateBalls(final int count) {
			super.updateBalls(count);

			if ( animVBO != null )
				animVBO.destroy();

			animVBO = new StreamVBO(GL_ARRAY_BUFFER, ballCount * (2 * 4));
		}

		public void render(final boolean render, final boolean animate, final int delta) {
			int batchSize = Math.min(ballCount, BALLS_PER_BATCH);
			int ballIndex = 0;
			while ( ballIndex < ballCount ) {
				if ( animate ) {
					final ByteBuffer buffer = animVBO.map(batchSize * (2 * 4));

					long t0 = System.nanoTime();
					animate(sprites, SpriteRender.<SpriteRender>map(buffer), ballSize, ballIndex, batchSize, delta);
					long t1 = System.nanoTime();

					animateTime += t1 - t0;

					animVBO.unmap();
				}

				if ( render ) {
					glVertexPointer(2, GL_FLOAT, 0, ballIndex * (2 * 4));
					glDrawArrays(GL_POINTS, 0, batchSize);
				}

				ballIndex += batchSize;
				batchSize = Math.min(ballCount - ballIndex, BALLS_PER_BATCH);
			}
		}

	}

	private class SpriteRendererTF extends SpriteRenderer {

		private int progIDTF;
		private int ballSizeLoc;
		private int deltaLoc;

		private int[] tfVBO = new int[2];
		private int currVBO;

		SpriteRendererTF() {
			System.out.println("Shootout Implementation: TF GPU animation");

			// Transform-feedback program

			final int vshID = glCreateShader(GL_VERTEX_SHADER);
			glShaderSource(vshID, "#version 130\n" +
			                      "const float WIDTH = " + SCREEN_WIDTH + ";\n" +
			                      "const float HEIGHT = " + SCREEN_HEIGHT + ";\n" +
			                      "uniform float ballSize;\n" + // ballSize / 2
			                      "uniform float delta;\n" +
			                      "void main(void) {\n" +
			                      "     vec4 anim = gl_Vertex;\n" +
			                      "     anim.xy = anim.xy + anim.zw * delta;\n" +
			                      "     vec2 animC = clamp(anim.xy, vec2(ballSize), vec2(WIDTH - ballSize, HEIGHT - ballSize));\n" +
			                      "     if ( anim.x != animC.x ) anim.z = -anim.z;\n" +
			                      "     if ( anim.y != animC.y ) anim.w = -anim.w;\n" +
			                      "     gl_Position = vec4(animC, anim.zw);\n" +
			                      "}");
			glCompileShader(vshID);
			if ( glGetShaderi(vshID, GL_COMPILE_STATUS) == GL_FALSE ) {
				System.out.println(glGetShaderInfoLog(vshID, glGetShaderi(vshID, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to compile vertex shader.");
			}

			progIDTF = glCreateProgram();
			glAttachShader(progIDTF, vshID);
			glTransformFeedbackVaryings(progIDTF, new CharSequence[] { "gl_Position" }, GL_SEPARATE_ATTRIBS);
			glLinkProgram(progIDTF);
			if ( glGetProgrami(progIDTF, GL_LINK_STATUS) == GL_FALSE ) {
				System.out.println(glGetProgramInfoLog(progIDTF, glGetProgrami(progIDTF, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to link shader program.");
			}

			glUseProgram(progIDTF);

			ballSizeLoc = glGetUniformLocation(progIDTF, "ballSize");
			deltaLoc = glGetUniformLocation(progIDTF, "delta");

			glUniform1f(ballSizeLoc, ballSize * 0.5f);

			// -----------------

			this.vshID = glCreateShader(GL_VERTEX_SHADER);
			glShaderSource(this.vshID, "void main(void) {\n" +
			                           "     gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n" +
			                           "}");
			glCompileShader(this.vshID);
			if ( glGetShaderi(this.vshID, GL_COMPILE_STATUS) == GL_FALSE ) {
				System.out.println(glGetShaderInfoLog(this.vshID, glGetShaderi(this.vshID, GL_INFO_LOG_LENGTH)));
				throw new RuntimeException("Failed to compile vertex shader.");
			}

			createProgram();
		}

		public void updateBallSize() {
			glUseProgram(progIDTF);
			glUniform1f(ballSizeLoc, ballSize * 0.5f);

			glUseProgram(progID);
			super.updateBallSize();
		}

		private void doUpdateBalls(final int count) {
			final Random random = new Random();

			final Sprite newSprites = Sprite.malloc(count);
			if ( sprites != null ) {
				sprites.view = 0;
				sprites.copyRange(newSprites, Math.min(count, spriteCount));
			}

			if ( count > spriteCount ) {
				for ( int i = spriteCount; i < count; i++ ) {
					newSprites.view = i;

					newSprites.x = (int)(random.nextFloat() * (SCREEN_WIDTH - ballSize) + ballSize * 0.5f);
					newSprites.y = (int)(random.nextFloat() * (SCREEN_HEIGHT - ballSize) + ballSize * 0.5f);
					newSprites.dx = random.nextFloat() * 0.4f - 0.2f;
					newSprites.dy = random.nextFloat() * 0.4f - 0.2f;
				}
			}

			sprites = newSprites;
			spriteCount = count;
		}

		public void updateBalls(final int count) {
			if ( tfVBO[0] != 0 ) {
				// Fetch current animation state
				glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sprites.backingByteBuffer());
			}

			doUpdateBalls(count);

			if ( tfVBO[0] != 0 ) {
				for ( int i = 0; i < tfVBO.length; i++ )
					glDeleteBuffers(tfVBO[i]);
			}

			for ( int i = 0; i < tfVBO.length; i++ ) {
				tfVBO[i] = glGenBuffers();
				glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, tfVBO[i]);
				glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sprites.backingByteBuffer(), GL_STATIC_DRAW);
			}

			glBindBuffer(GL_ARRAY_BUFFER, tfVBO[0]);
			glVertexPointer(2, GL_FLOAT, (4 * 4), 0);
		}

		public void render(final boolean render, final boolean animate, final int delta) {
			if ( animate ) {
				glUseProgram(progIDTF);
				glUniform1f(deltaLoc, delta);

				final int vbo = currVBO;
				currVBO = 1 - currVBO;

				glBindBuffer(GL_ARRAY_BUFFER, tfVBO[vbo]);
				glVertexPointer(4, GL_FLOAT, 0, 0);

				glEnable(GL_RASTERIZER_DISCARD);
				if ( GLContext.getCapabilities().OpenGL30 ) {
					glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tfVBO[1 - vbo]);

					glBeginTransformFeedback(GL_POINTS);
					glDrawArrays(GL_POINTS, 0, ballCount);
					glEndTransformFeedback();
				} else {
					glBindBufferBaseEXT(GL_TRANSFORM_FEEDBACK_BUFFER_EXT, 0, tfVBO[1 - vbo]);

					glBeginTransformFeedbackEXT(GL_POINTS);
					glDrawArrays(GL_POINTS, 0, ballCount);
					glEndTransformFeedbackEXT();
				}
				glDisable(GL_RASTERIZER_DISCARD);

				glUseProgram(progID);
				glVertexPointer(2, GL_FLOAT, (4 * 4), 0);
			}

			if ( render )
				glDrawArrays(GL_POINTS, 0, ballCount);
		}

	}

}
