From 1b22d810c56a065972fd5b7ad93f318bc7eb2a33 Mon Sep 17 00:00:00 2001 From: vegeta1k95 <32207817+vegeta1k95@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:45:05 +0100 Subject: [PATCH] Experimental: mitigate texture swapping with TextureArraySpriteBatch (#8604) * Performance optimization: less texture swapping * Added experimental setting to Options-Display --------- Co-authored-by: tunerzinc@gmail.com --- .../jsons/translations/English.properties | 2 + .../jsons/translations/template.properties | 1 + .../com/unciv/models/metadata/GameSettings.kt | 1 + core/src/com/unciv/ui/UncivStage.kt | 21 +- core/src/com/unciv/ui/options/DisplayTab.kt | 5 + .../ui/utils/TextureArraySpriteBatch.java | 1440 +++++++++++++++++ 6 files changed, 1469 insertions(+), 1 deletion(-) create mode 100644 core/src/com/unciv/ui/utils/TextureArraySpriteBatch.java diff --git a/android/assets/jsons/translations/English.properties b/android/assets/jsons/translations/English.properties index 21bb7a2f8d..4278ac2eae 100644 --- a/android/assets/jsons/translations/English.properties +++ b/android/assets/jsons/translations/English.properties @@ -1359,6 +1359,8 @@ Show unit movement arrows = # Requires translation! Continuous rendering = # Requires translation! +Experimental rendering improvements = + # Requires translation! When disabled, saves battery life but certain animations will be suspended = # Requires translation! Order trade offers by amount = diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 72e8eb7cc3..4d06c2b8fa 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -732,6 +732,7 @@ Enable display cutout (requires restart) = Show tile yields = Show unit movement arrows = Continuous rendering = +Experimental rendering improvements = When disabled, saves battery life but certain animations will be suspended = Order trade offers by amount = Ask for confirmation when pressing next turn = diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index da6d21fee7..4aca63f4fd 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -62,6 +62,7 @@ class GameSettings { val showPixelUnits: Boolean get() = unitSet != null var showPixelImprovements: Boolean = true var continuousRendering = false + var experimentalRendering = false var orderTradeOffersByAmount = true var confirmNextTurn = false var windowState = WindowState() diff --git a/core/src/com/unciv/ui/UncivStage.kt b/core/src/com/unciv/ui/UncivStage.kt index 5fbd063054..1fc104e3e5 100644 --- a/core/src/com/unciv/ui/UncivStage.kt +++ b/core/src/com/unciv/ui/UncivStage.kt @@ -1,18 +1,37 @@ package com.unciv.ui import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.utils.viewport.Viewport +import com.unciv.UncivGame import com.unciv.logic.event.Event import com.unciv.logic.event.EventBus import com.unciv.ui.crashhandling.wrapCrashHandling import com.unciv.ui.crashhandling.wrapCrashHandlingUnit +import com.unciv.ui.utils.TextureArraySpriteBatch import com.unciv.utils.Log /** Main stage for the game. Catches all exceptions or errors thrown by event handlers, calling [com.unciv.UncivGame.handleUncaughtThrowable] with the thrown exception or error. */ -class UncivStage(viewport: Viewport) : Stage(viewport) { +class UncivStage(viewport: Viewport) : Stage(viewport, getBatch()) { + + companion object { + fun getBatch(size: Int=1000): Batch { + // If for some reason it fails, we resort to usual SpriteBatch + return if (UncivGame.Current.settings.experimentalRendering) { + try { + TextureArraySpriteBatch(size) + } catch (e: Exception) { + SpriteBatch(size) + } + } else { + SpriteBatch(size) + } + } + } /** * Enables/disables sending pointer enter/exit events to actors on this stage. diff --git a/core/src/com/unciv/ui/options/DisplayTab.kt b/core/src/com/unciv/ui/options/DisplayTab.kt index eb84101dd0..088c21d997 100644 --- a/core/src/com/unciv/ui/options/DisplayTab.kt +++ b/core/src/com/unciv/ui/options/DisplayTab.kt @@ -69,6 +69,11 @@ fun displayTab( continuousRenderingLabel.wrap = true add(continuousRenderingLabel).colspan(2).padTop(10f).row() + optionsPopup.addCheckbox(this, "Experimental rendering improvements", settings.experimentalRendering) { + settings.experimentalRendering = it + onChange() + } + } private fun addMinimapSizeSlider(table: Table, settings: GameSettings, selectBoxMinWidth: Float) { diff --git a/core/src/com/unciv/ui/utils/TextureArraySpriteBatch.java b/core/src/com/unciv/ui/utils/TextureArraySpriteBatch.java new file mode 100644 index 0000000000..10bb536181 --- /dev/null +++ b/core/src/com/unciv/ui/utils/TextureArraySpriteBatch.java @@ -0,0 +1,1440 @@ +package com.unciv.ui.utils; + +import java.nio.IntBuffer; +import java.util.Arrays; + +import com.badlogic.gdx.Application.ApplicationType; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Mesh; +import com.badlogic.gdx.graphics.Mesh.VertexDataType; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.VertexAttribute; +import com.badlogic.gdx.graphics.VertexAttributes.Usage; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Affine2; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.utils.BufferUtils; + +/** Draws batched quads using indices. + *

+ * This is an optimized version of the SpriteBatch that maintains an LFU texture-cache to combine draw calls with different + * textures effectively. + *

+ * Use this Batch if you frequently utilize more than a single texture between calling {@link#begin()} and {@link#end()}. An + * example would be if your Atlas is spread over multiple Textures or if you draw with individual Textures. + * + * @see Batch + * @see SpriteBatch + * + * @author mzechner (Original SpriteBatch) + * @author Nathan Sweet (Original SpriteBatch) + * @author VaTTeRGeR (TextureArray Extension) */ + +public class TextureArraySpriteBatch implements Batch { + + private int idx = 0; + + private final Mesh mesh; + + private final float[] vertices; + + private final int spriteVertexSize = 5; + private final int spriteFloatSize = 20; + + /** The maximum number of available texture units for the fragment shader */ + private static int maxTextureUnits = -1; + + /** Textures in use (index: Texture Unit, value: Texture) */ + private final Texture[] usedTextures; + + /** LFU Array (index: Texture Unit Index - value: Access frequency) */ + private final int[] usedTexturesLFU; + + /** Gets sent to the fragment shader as an uniform "uniform sampler2d[X] u_textures" */ + private final IntBuffer textureUnitIndicesBuffer; + + private float invTexWidth = 0, invTexHeight = 0; + + private boolean drawing = false; + + private final Matrix4 transformMatrix = new Matrix4(); + private final Matrix4 projectionMatrix = new Matrix4(); + private final Matrix4 combinedMatrix = new Matrix4(); + + private boolean blendingDisabled = false; + private int blendSrcFunc = GL20.GL_SRC_ALPHA; + private int blendDstFunc = GL20.GL_ONE_MINUS_SRC_ALPHA; + private int blendSrcFuncAlpha = GL20.GL_SRC_ALPHA; + private int blendDstFuncAlpha = GL20.GL_ONE_MINUS_SRC_ALPHA; + + private ShaderProgram shader = null; + private ShaderProgram customShader = null; + + private static String shaderErrorLog = null; + + private boolean ownsShader; + + private final Color color = new Color(1, 1, 1, 1); + private float colorPacked = Color.WHITE_FLOAT_BITS; + + /** Number of render calls since the last {@link #begin()}. **/ + public int renderCalls = 0; + + /** Number of rendering calls, ever. Will not be reset unless set manually. **/ + public int totalRenderCalls = 0; + + /** The maximum number of sprites rendered in one batch so far. **/ + public int maxSpritesInBatch = 0; + + /** The current number of textures in the LFU cache. Gets reset when calling {@link#begin()} **/ + private int currentTextureLFUSize = 0; + + /** The current number of texture swaps in the LFU cache. Gets reset when calling {@link#begin()} **/ + private int currentTextureLFUSwaps = 0; + + /** Constructs a new TextureArraySpriteBatch with a size of 1000, one buffer, and the default shader. + * @see TextureArraySpriteBatch#TextureArraySpriteBatch(int, ShaderProgram) */ + public TextureArraySpriteBatch () { + this(1000); + } + + /** Constructs a TextureArraySpriteBatch with one buffer and the default shader. + * @see TextureArraySpriteBatch#TextureArraySpriteBatch(int, ShaderProgram) */ + public TextureArraySpriteBatch (int size) { + this(size, null); + } + + /** Constructs a new TextureArraySpriteBatch. Sets the projection matrix to an orthographic projection with y-axis point + * upwards, x-axis point to the right and the origin being in the bottom left corner of the screen. The projection will be + * pixel perfect with respect to the current screen resolution. + *

+ * The defaultShader specifies the shader to use. Note that the names for uniforms for this default shader are different than + * the ones expect for shaders set with {@link #setShader(ShaderProgram)}. + * @param size The max number of sprites in a single batch. Max of 8191. + * @param defaultShader The default shader to use. This is not owned by the TextureArraySpriteBatch and must be disposed + * separately. + * @throws IllegalStateException Thrown if the device does not support texture arrays. Make sure to implement a Fallback to + * {@link SpriteBatch} in case Texture Arrays are not supported on a clients device. + * @See {@link#createDefaultShader()} {@link#getMaxTextureUnits()} */ + public TextureArraySpriteBatch (int size, ShaderProgram defaultShader) throws IllegalStateException { + + // 32767 is max vertex index, so 32767 / 4 vertices per sprite = 8191 sprites max. + if (size > 8191) throw new IllegalArgumentException("Can't have more than 8191 sprites per batch: " + size); + + getMaxTextureUnits(); + + if (maxTextureUnits == 0) { + throw new IllegalStateException( + "Texture Arrays are not supported on this device:" + System.lineSeparator() + shaderErrorLog); + } + + if (defaultShader == null) { + shader = createDefaultShader(maxTextureUnits); + ownsShader = true; + + } else { + shader = defaultShader; + ownsShader = false; + } + + usedTextures = new Texture[maxTextureUnits]; + usedTexturesLFU = new int[maxTextureUnits]; + + // This contains the numbers 0 ... maxTextureUnits - 1. We send these to the shader as an uniform. + textureUnitIndicesBuffer = BufferUtils.newIntBuffer(maxTextureUnits); + for (int i = 0; i < maxTextureUnits; i++) { + textureUnitIndicesBuffer.put(i); + } + textureUnitIndicesBuffer.flip(); + + VertexDataType vertexDataType = (Gdx.gl30 != null) ? VertexDataType.VertexBufferObjectWithVAO : VertexDataType.VertexArray; + + // The vertex data is extended with one float for the texture index. + mesh = new Mesh(vertexDataType, false, size * 4, size * 6, + new VertexAttribute(Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE), + new VertexAttribute(Usage.ColorPacked, 4, ShaderProgram.COLOR_ATTRIBUTE), + new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0"), + new VertexAttribute(Usage.Generic, 1, "texture_index")); + + projectionMatrix.setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + + vertices = new float[size * (spriteFloatSize + 4)]; + + int len = size * 6; + short[] indices = new short[len]; + short j = 0; + for (int i = 0; i < len; i += 6, j += 4) { + indices[i] = j; + indices[i + 1] = (short)(j + 1); + indices[i + 2] = (short)(j + 2); + indices[i + 3] = (short)(j + 2); + indices[i + 4] = (short)(j + 3); + indices[i + 5] = j; + } + + mesh.setIndices(indices); + } + + /** Returns a new instance of the default shader used by TextureArraySpriteBatch for GL2 when no shader is specified. + * @See {@link#getMaxTextureUnits()} */ + public static ShaderProgram createDefaultShader (int maxTextureUnits) { + + // The texture index is just passed to the fragment shader, maybe there's an more elegant way. + String vertexShader = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + + "attribute float texture_index;\n" // + + "uniform mat4 u_projTrans;\n" // + + "varying vec4 v_color;\n" // + + "varying vec2 v_texCoords;\n" // + + "varying float v_texture_index;\n" // + + "\n" // + + "void main()\n" // + + "{\n" // + + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + + " v_color.a = v_color.a * (255.0/254.0);\n" // + + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + + " v_texture_index = texture_index;\n" // + + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + + "}\n"; + + // The texture is simply selected from an array of textures + String fragmentShader = "#ifdef GL_ES\n" // + + "#define LOWP lowp\n" // + + "precision mediump float;\n" // + + "#else\n" // + + "#define LOWP\n" // + + "#endif\n" // + + "varying LOWP vec4 v_color;\n" // + + "varying vec2 v_texCoords;\n" // + + "varying float v_texture_index;\n" // + + "uniform sampler2D u_textures[" + maxTextureUnits + "];\n" // + + "void main()\n"// + + "{\n" // + + " int index = int(v_texture_index);" // + + " gl_FragColor = v_color * texture2D(u_textures[index], v_texCoords);\n" // + + "}"; + + final ApplicationType appType = Gdx.app.getType(); + + if (appType == ApplicationType.Android || appType == ApplicationType.iOS || appType == ApplicationType.WebGL) { + fragmentShader = "#version 100\n" + fragmentShader; + } else { + fragmentShader = "#version 150\n" + fragmentShader; + } + + ShaderProgram shader = new ShaderProgram(vertexShader, fragmentShader); + + if (!shader.isCompiled()) { + throw new IllegalArgumentException("Error compiling shader: " + shader.getLog()); + } + + return shader; + } + + @Override + public void begin () { + + if (drawing) throw new IllegalStateException("TextureArraySpriteBatch.end must be called before begin."); + + renderCalls = 0; + + currentTextureLFUSize = 0; + currentTextureLFUSwaps = 0; + + Arrays.fill(usedTextures, null); + Arrays.fill(usedTexturesLFU, 0); + + Gdx.gl.glDepthMask(false); + + if (customShader != null) { + customShader.begin(); + } else { + shader.begin(); + } + + setupMatrices(); + + drawing = true; + } + + @Override + public void end () { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before end."); + + if (idx > 0) flush(); + + drawing = false; + + GL20 gl = Gdx.gl; + + gl.glDepthMask(true); + + if (isBlendingEnabled()) { + gl.glDisable(GL20.GL_BLEND); + } + + if (customShader != null) { + customShader.end(); + } else { + shader.end(); + } + } + + @Override + public void dispose () { + + mesh.dispose(); + + if (ownsShader && shader != null) { + shader.dispose(); + } + } + + @Override + public void setColor (Color tint) { + color.set(tint); + colorPacked = tint.toFloatBits(); + } + + @Override + public void setColor (float r, float g, float b, float a) { + color.set(r, g, b, a); + colorPacked = color.toFloatBits(); + } + + @Override + public Color getColor () { + return color; + } + + @Override + public void setPackedColor (float packedColor) { + Color.abgr8888ToColor(color, packedColor); + this.colorPacked = packedColor; + } + + @Override + public float getPackedColor () { + return colorPacked; + } + + @Override + public void draw (Texture texture, float x, float y, float originX, float originY, float width, float height, float scaleX, + float scaleY, float rotation, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY) { + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(texture); + + // bottom left and top right corner points relative to origin + final float worldOriginX = x + originX; + final float worldOriginY = y + originY; + float fx = -originX; + float fy = -originY; + float fx2 = width - originX; + float fy2 = height - originY; + + // scale + if (scaleX != 1 || scaleY != 1) { + fx *= scaleX; + fy *= scaleY; + fx2 *= scaleX; + fy2 *= scaleY; + } + + // construct corner points, start from top left and go counter clockwise + final float p1x = fx; + final float p1y = fy; + final float p2x = fx; + final float p2y = fy2; + final float p3x = fx2; + final float p3y = fy2; + final float p4x = fx2; + final float p4y = fy; + + float x1; + float y1; + float x2; + float y2; + float x3; + float y3; + float x4; + float y4; + + // rotate + if (rotation != 0) { + final float cos = MathUtils.cosDeg(rotation); + final float sin = MathUtils.sinDeg(rotation); + + x1 = cos * p1x - sin * p1y; + y1 = sin * p1x + cos * p1y; + + x2 = cos * p2x - sin * p2y; + y2 = sin * p2x + cos * p2y; + + x3 = cos * p3x - sin * p3y; + y3 = sin * p3x + cos * p3y; + + x4 = x1 + (x3 - x2); + y4 = y3 - (y2 - y1); + } else { + x1 = p1x; + y1 = p1y; + + x2 = p2x; + y2 = p2y; + + x3 = p3x; + y3 = p3y; + + x4 = p4x; + y4 = p4y; + } + + x1 += worldOriginX; + y1 += worldOriginY; + x2 += worldOriginX; + y2 += worldOriginY; + x3 += worldOriginX; + y3 += worldOriginY; + x4 += worldOriginX; + y4 += worldOriginY; + + float u = srcX * invTexWidth; + float v = (srcY + srcHeight) * invTexHeight; + float u2 = (srcX + srcWidth) * invTexWidth; + float v2 = srcY * invTexHeight; + + if (flipX) { + float tmp = u; + u = u2; + u2 = tmp; + } + + if (flipY) { + float tmp = v; + v = v2; + v2 = tmp; + } + + final float color = this.colorPacked; + + vertices[idx++] = x1; + vertices[idx++] = y1; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x2; + vertices[idx++] = y2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x3; + vertices[idx++] = y3; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x4; + vertices[idx++] = y4; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (Texture texture, float x, float y, float width, float height, int srcX, int srcY, int srcWidth, + int srcHeight, boolean flipX, boolean flipY) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(texture); + + float u = srcX * invTexWidth; + float v = (srcY + srcHeight) * invTexHeight; + float u2 = (srcX + srcWidth) * invTexWidth; + float v2 = srcY * invTexHeight; + final float fx2 = x + width; + final float fy2 = y + height; + + if (flipX) { + float tmp = u; + u = u2; + u2 = tmp; + } + + if (flipY) { + float tmp = v; + v = v2; + v2 = tmp; + } + + float color = this.colorPacked; + + vertices[idx++] = x; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight) { + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(texture); + + final float u = srcX * invTexWidth; + final float v = (srcY + srcHeight) * invTexHeight; + final float u2 = (srcX + srcWidth) * invTexWidth; + final float v2 = srcY * invTexHeight; + final float fx2 = x + srcWidth; + final float fy2 = y + srcHeight; + + float color = this.colorPacked; + + vertices[idx++] = x; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(texture); + + final float fx2 = x + width; + final float fy2 = y + height; + + float color = this.colorPacked; + + vertices[idx++] = x; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (Texture texture, float x, float y) { + draw(texture, x, y, texture.getWidth(), texture.getHeight()); + } + + @Override + public void draw (Texture texture, float x, float y, float width, float height) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(texture); + + final float fx2 = x + width; + final float fy2 = y + height; + final float u = 0; + final float v = 1; + final float u2 = 1; + final float v2 = 0; + + float color = this.colorPacked; + + vertices[idx++] = x; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (Texture texture, float[] spriteVertices, int offset, int count) { + + if (!drawing) { + throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + } + + flushIfFull(); + + // Assigns a texture unit to this texture, flushing if none is available + final float ti = (float)activateTexture(texture); + + // spriteVertexSize is the number of floats an unmodified input vertex consists of, + // therefore this loop iterates over the vertices stored in parameter spriteVertices. + for (int srcPos = 0; srcPos < count; srcPos += spriteVertexSize) { + + // Copy the vertices + System.arraycopy(spriteVertices, srcPos, vertices, idx, spriteVertexSize); + + // Advance idx by vertex float count + idx += spriteVertexSize; + + // Inject texture unit index and advance idx + vertices[idx++] = ti; + } + } + + @Override + public void draw (TextureRegion region, float x, float y) { + draw(region, x, y, region.getRegionWidth(), region.getRegionHeight()); + } + + @Override + public void draw (TextureRegion region, float x, float y, float width, float height) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(region.getTexture()); + + final float fx2 = x + width; + final float fy2 = y + height; + final float u = region.getU(); + final float v = region.getV2(); + final float u2 = region.getU2(); + final float v2 = region.getV(); + + float color = this.colorPacked; + + vertices[idx++] = x; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = fy2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = fx2; + vertices[idx++] = y; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (TextureRegion region, float x, float y, float originX, float originY, float width, float height, + float scaleX, float scaleY, float rotation) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(region.getTexture()); + + // bottom left and top right corner points relative to origin + final float worldOriginX = x + originX; + final float worldOriginY = y + originY; + float fx = -originX; + float fy = -originY; + float fx2 = width - originX; + float fy2 = height - originY; + + // scale + if (scaleX != 1 || scaleY != 1) { + fx *= scaleX; + fy *= scaleY; + fx2 *= scaleX; + fy2 *= scaleY; + } + + // construct corner points, start from top left and go counter clockwise + final float p1x = fx; + final float p1y = fy; + final float p2x = fx; + final float p2y = fy2; + final float p3x = fx2; + final float p3y = fy2; + final float p4x = fx2; + final float p4y = fy; + + float x1; + float y1; + float x2; + float y2; + float x3; + float y3; + float x4; + float y4; + + // rotate + if (rotation != 0) { + final float cos = MathUtils.cosDeg(rotation); + final float sin = MathUtils.sinDeg(rotation); + + x1 = cos * p1x - sin * p1y; + y1 = sin * p1x + cos * p1y; + + x2 = cos * p2x - sin * p2y; + y2 = sin * p2x + cos * p2y; + + x3 = cos * p3x - sin * p3y; + y3 = sin * p3x + cos * p3y; + + x4 = x1 + (x3 - x2); + y4 = y3 - (y2 - y1); + } else { + x1 = p1x; + y1 = p1y; + + x2 = p2x; + y2 = p2y; + + x3 = p3x; + y3 = p3y; + + x4 = p4x; + y4 = p4y; + } + + x1 += worldOriginX; + y1 += worldOriginY; + x2 += worldOriginX; + y2 += worldOriginY; + x3 += worldOriginX; + y3 += worldOriginY; + x4 += worldOriginX; + y4 += worldOriginY; + + final float u = region.getU(); + final float v = region.getV2(); + final float u2 = region.getU2(); + final float v2 = region.getV(); + + float color = this.colorPacked; + + vertices[idx++] = x1; + vertices[idx++] = y1; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x2; + vertices[idx++] = y2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x3; + vertices[idx++] = y3; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x4; + vertices[idx++] = y4; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + @Override + public void draw (TextureRegion region, float x, float y, float originX, float originY, float width, float height, + float scaleX, float scaleY, float rotation, boolean clockwise) { + + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(region.getTexture()); + + // bottom left and top right corner points relative to origin + final float worldOriginX = x + originX; + final float worldOriginY = y + originY; + float fx = -originX; + float fy = -originY; + float fx2 = width - originX; + float fy2 = height - originY; + + // scale + if (scaleX != 1 || scaleY != 1) { + fx *= scaleX; + fy *= scaleY; + fx2 *= scaleX; + fy2 *= scaleY; + } + + // construct corner points, start from top left and go counter clockwise + final float p1x = fx; + final float p1y = fy; + final float p2x = fx; + final float p2y = fy2; + final float p3x = fx2; + final float p3y = fy2; + final float p4x = fx2; + final float p4y = fy; + + float x1; + float y1; + float x2; + float y2; + float x3; + float y3; + float x4; + float y4; + + // rotate + if (rotation != 0) { + final float cos = MathUtils.cosDeg(rotation); + final float sin = MathUtils.sinDeg(rotation); + + x1 = cos * p1x - sin * p1y; + y1 = sin * p1x + cos * p1y; + + x2 = cos * p2x - sin * p2y; + y2 = sin * p2x + cos * p2y; + + x3 = cos * p3x - sin * p3y; + y3 = sin * p3x + cos * p3y; + + x4 = x1 + (x3 - x2); + y4 = y3 - (y2 - y1); + } else { + x1 = p1x; + y1 = p1y; + + x2 = p2x; + y2 = p2y; + + x3 = p3x; + y3 = p3y; + + x4 = p4x; + y4 = p4y; + } + + x1 += worldOriginX; + y1 += worldOriginY; + x2 += worldOriginX; + y2 += worldOriginY; + x3 += worldOriginX; + y3 += worldOriginY; + x4 += worldOriginX; + y4 += worldOriginY; + + float u1, v1, u2, v2, u3, v3, u4, v4; + if (clockwise) { + u1 = region.getU2(); + v1 = region.getV2(); + u2 = region.getU(); + v2 = region.getV2(); + u3 = region.getU(); + v3 = region.getV(); + u4 = region.getU2(); + v4 = region.getV(); + } else { + u1 = region.getU(); + v1 = region.getV(); + u2 = region.getU2(); + v2 = region.getV(); + u3 = region.getU2(); + v3 = region.getV2(); + u4 = region.getU(); + v4 = region.getV2(); + } + + float color = this.colorPacked; + + vertices[idx++] = x1; + vertices[idx++] = y1; + vertices[idx++] = color; + vertices[idx++] = u1; + vertices[idx++] = v1; + vertices[idx++] = ti; + + vertices[idx++] = x2; + vertices[idx++] = y2; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x3; + vertices[idx++] = y3; + vertices[idx++] = color; + vertices[idx++] = u3; + vertices[idx++] = v3; + vertices[idx++] = ti; + + vertices[idx++] = x4; + vertices[idx++] = y4; + vertices[idx++] = color; + vertices[idx++] = u4; + vertices[idx++] = v4; + vertices[idx++] = ti; + } + + @Override + public void draw (TextureRegion region, float width, float height, Affine2 transform) { + if (!drawing) throw new IllegalStateException("TextureArraySpriteBatch.begin must be called before draw."); + + float[] vertices = this.vertices; + + flushIfFull(); + + final float ti = activateTexture(region.getTexture()); + + // construct corner points + float x1 = transform.m02; + float y1 = transform.m12; + float x2 = transform.m01 * height + transform.m02; + float y2 = transform.m11 * height + transform.m12; + float x3 = transform.m00 * width + transform.m01 * height + transform.m02; + float y3 = transform.m10 * width + transform.m11 * height + transform.m12; + float x4 = transform.m00 * width + transform.m02; + float y4 = transform.m10 * width + transform.m12; + + float u = region.getU(); + float v = region.getV2(); + float u2 = region.getU2(); + float v2 = region.getV(); + + float color = this.colorPacked; + + vertices[idx++] = x1; + vertices[idx++] = y1; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v; + vertices[idx++] = ti; + + vertices[idx++] = x2; + vertices[idx++] = y2; + vertices[idx++] = color; + vertices[idx++] = u; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x3; + vertices[idx++] = y3; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v2; + vertices[idx++] = ti; + + vertices[idx++] = x4; + vertices[idx++] = y4; + vertices[idx++] = color; + vertices[idx++] = u2; + vertices[idx++] = v; + vertices[idx++] = ti; + } + + /** Flushes if the vertices array cannot hold an additional sprite ((spriteVertexSize + 1) * 4 vertices) anymore. */ + private void flushIfFull () { + // original Sprite attribute size plus one extra float per sprite vertex + if (vertices.length - idx < spriteFloatSize + spriteFloatSize / spriteVertexSize) { + flush(); + } + } + + @Override + public void flush () { + + if (idx == 0) return; + + renderCalls++; + totalRenderCalls++; + + int spritesInBatch = idx / (spriteFloatSize + 4); + if (spritesInBatch > maxSpritesInBatch) maxSpritesInBatch = spritesInBatch; + int count = spritesInBatch * 6; + + // Bind the textures + for (int i = 0; i < currentTextureLFUSize; i++) { + usedTextures[i].bind(i); + } + + // Set TEXTURE0 as active again before drawing. + Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0); + + Mesh mesh = this.mesh; + + mesh.setVertices(vertices, 0, idx); + + mesh.getIndicesBuffer().position(0); + mesh.getIndicesBuffer().limit(count); + + if (blendingDisabled) { + Gdx.gl.glDisable(GL20.GL_BLEND); + } else { + Gdx.gl.glEnable(GL20.GL_BLEND); + if (blendSrcFunc != -1) Gdx.gl.glBlendFuncSeparate(blendSrcFunc, blendDstFunc, blendSrcFuncAlpha, blendDstFuncAlpha); + } + + if (customShader != null) { + mesh.render(customShader, GL20.GL_TRIANGLES, 0, count); + } else { + mesh.render(shader, GL20.GL_TRIANGLES, 0, count); + } + + idx = 0; + } + + /** Assigns Texture units and manages the LFU cache. + * @param texture The texture that shall be loaded into the cache, if it is not already loaded. + * @return The texture slot that has been allocated to the selected texture */ + private int activateTexture (Texture texture) { + + invTexWidth = 1.0f / texture.getWidth(); + invTexHeight = 1.0f / texture.getHeight(); + + // This is our identifier for the textures + final int textureHandle = texture.getTextureObjectHandle(); + + // First try to see if the texture is already cached + for (int i = 0; i < currentTextureLFUSize; i++) { + + // getTextureObjectHandle() just returns an int, + // it's fine to call this method instead of caching the value. + if (textureHandle == usedTextures[i].getTextureObjectHandle()) { + + // Increase the access counter. + usedTexturesLFU[i]++; + + return i; + } + } + + // If a free texture unit is available we just use it + // If not we have to flush and then throw out the least accessed one. + if (currentTextureLFUSize < maxTextureUnits) { + + // Put the texture into the next free slot + usedTextures[currentTextureLFUSize] = texture; + + // Increase the access counter. + usedTexturesLFU[currentTextureLFUSize]++; + + return currentTextureLFUSize++; + + } else { + + // We have to flush if there is something in the pipeline already, + // otherwise the texture index of previously rendered sprites gets invalidated + if (idx > 0) { + flush(); + } + + int slot = 0; + int slotVal = usedTexturesLFU[0]; + + int max = 0; + int average = 0; + + // We search for the best candidate for a swap (least accessed) and collect some data + for (int i = 0; i < maxTextureUnits; i++) { + + final int val = usedTexturesLFU[i]; + + max = Math.max(val, max); + + average += val; + + if (val <= slotVal) { + slot = i; + slotVal = val; + } + } + + // The LFU weights will be normalized to the range 0...100 + final int normalizeRange = 100; + + for (int i = 0; i < maxTextureUnits; i++) { + usedTexturesLFU[i] = usedTexturesLFU[i] * normalizeRange / max; + } + + average = (average * normalizeRange) / (max * maxTextureUnits); + + // Give the new texture a fair (average) chance of staying. + usedTexturesLFU[slot] = average; + + usedTextures[slot] = texture; + + // For statistics + currentTextureLFUSwaps++; + + return slot; + } + } + + /** @return The number of texture swaps the LFU cache performed since calling {@link#begin()}. */ + public int getTextureLFUSwaps () { + return currentTextureLFUSwaps; + } + + /** @return The current number of textures in the LFU cache. Gets reset when calling {@link#begin()}. */ + public int getTextureLFUSize () { + return currentTextureLFUSize; + } + + /** @return The maximum number of textures that the LFU cache can hold. This limit is imposed by the driver. + * @see {@link #getMaxTextureUnits()} */ + public int getTextureLFUCapacity () { + return getMaxTextureUnits(); + } + + @Override + public void disableBlending () { + + if (blendingDisabled) return; + + flush(); + + blendingDisabled = true; + } + + @Override + public void enableBlending () { + + if (!blendingDisabled) { + return; + } + + flush(); + + blendingDisabled = false; + } + + @Override + public void setBlendFunction (int srcFunc, int dstFunc) { + setBlendFunctionSeparate(srcFunc, dstFunc, srcFunc, dstFunc); + } + + @Override + public void setBlendFunctionSeparate (int srcFuncColor, int dstFuncColor, int srcFuncAlpha, int dstFuncAlpha) { + + if (blendSrcFunc == srcFuncColor && blendDstFunc == dstFuncColor && blendSrcFuncAlpha == srcFuncAlpha + && blendDstFuncAlpha == dstFuncAlpha) { + return; + } + + flush(); + + blendSrcFunc = srcFuncColor; + blendDstFunc = dstFuncColor; + blendSrcFuncAlpha = srcFuncAlpha; + blendDstFuncAlpha = dstFuncAlpha; + } + + @Override + public int getBlendSrcFunc () { + return blendSrcFunc; + } + + @Override + public int getBlendDstFunc () { + return blendDstFunc; + } + + @Override + public int getBlendSrcFuncAlpha () { + return blendSrcFuncAlpha; + } + + @Override + public int getBlendDstFuncAlpha () { + return blendDstFuncAlpha; + } + + @Override + public boolean isBlendingEnabled () { + return !blendingDisabled; + } + + @Override + public boolean isDrawing () { + return drawing; + } + + @Override + public Matrix4 getProjectionMatrix () { + return projectionMatrix; + } + + @Override + public Matrix4 getTransformMatrix () { + return transformMatrix; + } + + @Override + public void setProjectionMatrix (Matrix4 projection) { + + if (drawing) { + flush(); + } + + projectionMatrix.set(projection); + + if (drawing) { + setupMatrices(); + } + } + + @Override + public void setTransformMatrix (Matrix4 transform) { + + if (drawing) { + flush(); + } + + transformMatrix.set(transform); + + if (drawing) { + setupMatrices(); + } + } + + private void setupMatrices () { + + combinedMatrix.set(projectionMatrix).mul(transformMatrix); + + if (customShader != null) { + customShader.setUniformMatrix("u_projTrans", combinedMatrix); + Gdx.gl20.glUniform1iv(customShader.fetchUniformLocation("u_textures", true), maxTextureUnits, textureUnitIndicesBuffer); + + } else { + shader.setUniformMatrix("u_projTrans", combinedMatrix); + Gdx.gl20.glUniform1iv(shader.fetchUniformLocation("u_textures", true), maxTextureUnits, textureUnitIndicesBuffer); + } + } + + /** Queries the number of supported textures in a texture array by trying the create the default shader.
+ * The first call of this method is very expensive, after that it simply returns a cached value. + * @return the number of supported textures in a texture array or zero if this feature is unsupported on this device. + * @see {@link #setShader(ShaderProgram shader)} */ + public static int getMaxTextureUnits () { + + if (maxTextureUnits == -1) { + + // Query the number of available texture units and decide on a safe number of texture units to use + IntBuffer texUnitsQueryBuffer = BufferUtils.newIntBuffer(32); + + Gdx.gl.glGetIntegerv(GL20.GL_MAX_TEXTURE_IMAGE_UNITS, texUnitsQueryBuffer); + + int maxTextureUnitsLocal = texUnitsQueryBuffer.get(); + + // Some OpenGL drivers (I'm looking at you, Intel!) do not report the right values, + // so we take caution and test it first, reducing the number of slots if needed. + // Will try to find the maximum amount of texture units supported. + while (maxTextureUnitsLocal > 0) { + + try { + + ShaderProgram tempProg = createDefaultShader(maxTextureUnitsLocal); + + tempProg.dispose(); + + break; + + } catch (Exception e) { + + maxTextureUnitsLocal /= 2; + + shaderErrorLog = e.getMessage(); + } + } + + maxTextureUnits = maxTextureUnitsLocal; + } + + return maxTextureUnits; + } + + /** Sets the shader to be used in a GLES 2.0 environment. Vertex position attribute is called "a_position", the texture + * coordinates attribute is called "a_texCoord0", the color attribute is called "a_color", texture unit index is called + * "texture_index", this needs to be converted to int with int(...) in the fragment shader. See + * {@link ShaderProgram#POSITION_ATTRIBUTE}, {@link ShaderProgram#COLOR_ATTRIBUTE} and {@link ShaderProgram#TEXCOORD_ATTRIBUTE} + * which gets "0" appended to indicate the use of the first texture unit. The combined transform and projection matrix is + * uploaded via a mat4 uniform called "u_projTrans". The texture sampler array is passed via a uniform called "u_textures", see + * {@link TextureArraySpriteBatch#createDefaultShader(int)} for reference. + *

+ * Call this method with a null argument to use the default shader. + *

+ * This method will flush the batch before setting the new shader, you can call it in between {@link #begin()} and + * {@link #end()}. + * @param shader the {@link ShaderProgram} or null to use the default shader. + * @See {@link#createDefaultShader()} {@link#getMaxTextureUnits()} */ + @Override + public void setShader (ShaderProgram shader) { + + if (drawing) { + + flush(); + + if (customShader != null) { + customShader.end(); + } else { + this.shader.end(); + } + } + + customShader = shader; + + if (drawing) { + + if (customShader != null) { + customShader.begin(); + } else { + this.shader.begin(); + } + + setupMatrices(); + } + } + + @Override + public ShaderProgram getShader () { + + if (customShader != null) { + return customShader; + } else { + return shader; + } + } +}