/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.graphics; import java.nio.ByteBuffer; import java.nio.ByteOrder; import jpcsp.Memory; import jpcsp.graphics.RE.IRenderingEngine; import jpcsp.memory.IMemoryReader; import jpcsp.memory.MemoryReader; // Based on soywiz/pspemulator public class VertexInfo { // vtype public static final int vtypeMask = 0x009DDFFF; public boolean transform2D; public int skinningWeightCount; public int morphingVertexCount; public int texture; public int color; public int normal; public int position; public int weight; public int index; public int vtype; // vaddr, iaddr public int ptr_vertex; public int ptr_index; // other data public int vertexSize; public int oneVertexSize; public int textureOffset; public int colorOffset; public int normalOffset; public int positionOffset; public int alignmentSize; public static int[] size_mapping = new int[]{0, 1, 2, 4}; private static int[] size_padding = new int[]{0, 0, 1, 3}; private static int[] color_size_mapping = new int[]{0, 1, 1, 1, 2, 2, 2, 4}; private static int[] color_size_padding = new int[]{0, 0, 0, 0, 1, 1, 1, 3}; private float[] morph_weight = new float[8]; private static String[] texture_info = new String[]{ null, "GU_TEXTURE_8BIT", "GU_TEXTURE_16BIT", "GU_TEXTURE_32BITF" }; private static String[] color_info = new String[]{ null, "GU_COLOR_UNK2", "GU_COLOR_UNK3", "GU_COLOR_UNK4", "GU_COLOR_5650", "GU_COLOR_5551", "GU_COLOR_4444", "GU_COLOR_8888" }; private static String[] normal_info = new String[]{ null, "GU_NORMAL_8BIT", "GU_NORMAL_16BIT", "GU_NORMAL_32BITF" }; private static String[] vertex_info = new String[]{ null, "GU_VERTEX_8BIT", "GU_VERTEX_16BIT", "GU_VERTEX_32BITF" }; private static String[] weight_info = new String[]{ null, "GU_WEIGHT_8BIT", "GU_WEIGHT_16BIT", "GU_WEIGHT_32BITF" }; private static String[] index_info = new String[]{ null, "GU_INDEX_8BIT", "GU_INDEX_16BIT", "GU_INDEX_UNK3" }; private static String[] transform_info = new String[]{ "GU_TRANSFORM_3D", "GU_TRANSFORM_2D" }; private VertexInfoReaderTemplate vertexInfoReader; public boolean readTexture; // cache data private int bufferId = -1; // id created by glGenBuffers private int vertexArrayId = -1; private int[] cachedVertices; private int[] cachedIndices; private int cachedNumberOfVertex; private float[] cachedMorphWeights; private float[][] cachedBoneMatrix; private ByteBuffer cachedBuffer; private VertexCache vertexCache; public VertexInfo() { } public VertexInfo(VertexInfo vertexInfo) { vtype = vertexInfo.vtype; transform2D = vertexInfo.transform2D; skinningWeightCount = vertexInfo.skinningWeightCount; morphingVertexCount = vertexInfo.morphingVertexCount; texture = vertexInfo.texture; color = vertexInfo.color; normal = vertexInfo.normal; position = vertexInfo.position; weight = vertexInfo.weight; index = vertexInfo.index; ptr_vertex = vertexInfo.ptr_vertex; ptr_index = vertexInfo.ptr_index; vertexSize = vertexInfo.vertexSize; oneVertexSize = vertexInfo.oneVertexSize; textureOffset = vertexInfo.textureOffset; colorOffset = vertexInfo.colorOffset; normalOffset = vertexInfo.normalOffset; positionOffset = vertexInfo.positionOffset; alignmentSize = vertexInfo.alignmentSize; morph_weight = vertexInfo.morph_weight; cachedIndices = vertexInfo.cachedIndices; cachedVertices = vertexInfo.cachedVertices; vertexCache = vertexInfo.vertexCache; } public void processType(int param) { vtype = param & vtypeMask; updateVertexInfoReader(texture != 0); vertexInfoReader.processType(this); } public static void processType(VertexInfo vinfo, int vtype) { vinfo.vtype = vtype & vtypeMask; vinfo.texture = (vtype >> 0) & 0x3; vinfo.color = (vtype >> 2) & 0x7; vinfo.normal = (vtype >> 5) & 0x3; vinfo.position = (vtype >> 7) & 0x3; vinfo.weight = (vtype >> 9) & 0x3; vinfo.index = (vtype >> 11) & 0x3; vinfo.skinningWeightCount = ((vtype >> 14) & 0x7) + 1; vinfo.morphingVertexCount = ((vtype >> 18) & 0x7) + 1; vinfo.transform2D = ((vtype >> 23) & 0x1) != 0; int vertexSize = 0; vertexSize += size_mapping[vinfo.weight] * vinfo.skinningWeightCount; vertexSize = (vertexSize + size_padding[vinfo.texture]) & ~size_padding[vinfo.texture]; vinfo.textureOffset = vertexSize; vertexSize += size_mapping[vinfo.texture] * 2; vertexSize = (vertexSize + color_size_padding[vinfo.color]) & ~color_size_padding[vinfo.color]; vinfo.colorOffset = vertexSize; vertexSize += color_size_mapping[vinfo.color]; vertexSize = (vertexSize + size_padding[vinfo.normal]) & ~size_padding[vinfo.normal]; vinfo.normalOffset = vertexSize; vertexSize += size_mapping[vinfo.normal] * 3; vertexSize = (vertexSize + size_padding[vinfo.position]) & ~size_padding[vinfo.position]; vinfo.positionOffset = vertexSize; vertexSize += size_mapping[vinfo.position] * 3; vinfo.alignmentSize = Math.max(size_mapping[vinfo.weight], Math.max(color_size_mapping[vinfo.color], Math.max(size_mapping[vinfo.normal], Math.max(size_mapping[vinfo.texture], size_mapping[vinfo.position])))); vertexSize = (vertexSize + vinfo.alignmentSize - 1) & ~(vinfo.alignmentSize - 1); vinfo.oneVertexSize = vertexSize; vinfo.vertexSize = vertexSize * vinfo.morphingVertexCount; } private void updateVertexInfoReader(boolean readTexture) { this.readTexture = readTexture; vertexInfoReader = VertexInfoCompiler.getInstance().getCompiledVertexInfoReader(vtype, readTexture); } public int getAddress(Memory mem, int i) { if (ptr_index != 0 && index != 0) { int addr = ptr_index + i * index; switch (index) { case 1: i = mem.read8(addr); break; // GU_INDEX_8BIT case 2: i = mem.read16(addr); break; // GU_INDEX_16BIT case 3: i = mem.read32(addr); break; // GU_INDEX_UNK3 (assume 32bit) } } return ptr_vertex + i * vertexSize; } public void setMorphWeights(float[] mw) { morph_weight = mw; if (morphingVertexCount == 1) { morph_weight[0] = 1.f; } } public VertexState readVertex(Memory mem, int addr, boolean readTexture, boolean doubleTexture2DCoords) { VertexState v = new VertexState(); readVertex(mem, addr, v, readTexture, doubleTexture2DCoords); return v; } public void readVertex(Memory mem, int addr, VertexState v, boolean readTexture, boolean doubleTexture2DCoords) { if (texture == 0) { readTexture = false; } if (readTexture != this.readTexture) { updateVertexInfoReader(readTexture); } vertexInfoReader.readVertex(mem, addr, v, morph_weight); // HD Remaster can require to double the 2D texture coordinates if (doubleTexture2DCoords && transform2D && readTexture) { v.t[0] *= 2f; v.t[1] *= 2f; } } public void setDirty() { cachedIndices = null; cachedVertices = null; } private boolean equals(int[] a, int[] b) { if (a == null) { if (b != null) { return false; } } else { if (b == null) { return false; } if (a.length != b.length) { return false; } for (int i = 0; i < a.length; i++) { if (a[i] != b[i]) { return false; } } } return true; } public boolean equals(VertexInfo vertexInfo, int numberOfVertex) { // Do not compare the vertices and indices of the new vertex if it has already // been checked during this display cycle if (!vertexCache.vertexAlreadyChecked(vertexInfo)) { vertexInfo.readForCache(numberOfVertex); if (!equals(cachedVertices, vertexInfo.cachedVertices)) { return false; } if (!equals(cachedIndices, vertexInfo.cachedIndices)) { return false; } vertexCache.setVertexAlreadyChecked(vertexInfo); } else { if (index != 0 && cachedIndices == null) { return false; } if (ptr_vertex != 0 && cachedVertices == null) { return false; } } return true; } public boolean equals(VertexInfo vertexInfo, int numberOfVertex, float[][] boneMatrix, int numberOfWeightsForBuffer) { if (vtype != vertexInfo.vtype || cachedNumberOfVertex != numberOfVertex || ptr_index != vertexInfo.ptr_index) { return false; } if (morphingVertexCount > 1) { for (int i = 0; i < morphingVertexCount; i++) { if (cachedMorphWeights[i] != vertexInfo.morph_weight[i]) { return false; } } } // Check if the bone matrix has changed, only if not using Skinning Shaders if (weight != 0 && numberOfWeightsForBuffer == 0 && boneMatrix != null) { for (int i = 0; i < skinningWeightCount; i++) { for (int j = 0; j < 12; j++) { if (cachedBoneMatrix[i][j] != boneMatrix[i][j]) { return false; } } } } if (VideoEngine.getInstance().useOptimisticVertexCache) { if (index != 0 && cachedIndices == null) { return false; } if (ptr_vertex != 0 && cachedVertices == null) { return false; } return true; } return equals(vertexInfo, numberOfVertex); } public boolean bindVertex(IRenderingEngine re) { return bindVertex(re, false); } private boolean bindVertex(IRenderingEngine re, boolean bindBuffer) { boolean needSetDataPointers; if (vertexArrayId >= 0) { re.bindVertexArray(vertexArrayId); needSetDataPointers = false; if (bindBuffer) { re.bindBuffer(IRenderingEngine.RE_ARRAY_BUFFER, bufferId); } } else { re.bindBuffer(IRenderingEngine.RE_ARRAY_BUFFER, bufferId); needSetDataPointers = true; } return needSetDataPointers; } public boolean loadVertex(IRenderingEngine re, float[] buffer, int size) { boolean needSetDataPointers = false; if (vertexArrayId == -1 && re.isVertexArrayAvailable()) { vertexArrayId = re.genVertexArray(); needSetDataPointers = true; } if (bufferId == -1) { bufferId = re.genBuffer(); needSetDataPointers = true; } if (bindVertex(re, true)) { needSetDataPointers = true; } int bufferSize = size * VideoEngine.SIZEOF_FLOAT; if (cachedBuffer == null || cachedBuffer.capacity() < bufferSize) { cachedBuffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.LITTLE_ENDIAN); } else { cachedBuffer.clear(); } cachedBuffer.asFloatBuffer().put(buffer, 0, size); cachedBuffer.rewind(); re.setBufferData(IRenderingEngine.RE_ARRAY_BUFFER, size * VideoEngine.SIZEOF_FLOAT, cachedBuffer, IRenderingEngine.RE_STATIC_DRAW); return needSetDataPointers; } public void deleteVertex(IRenderingEngine re) { if (bufferId != -1) { re.deleteBuffer(bufferId); bufferId = -1; } if (vertexArrayId != -1) { re.deleteVertexArray(vertexArrayId); vertexArrayId = -1; } cachedMorphWeights = null; cachedBoneMatrix = null; cachedBuffer = null; cachedIndices = null; cachedVertices = null; } private void readForCache(int numberOfVertex) { if (cachedIndices != null || cachedVertices != null) { return; } int vertexArraySize; if (ptr_index != 0 && index != 0) { IMemoryReader memoryReader = null; switch (index) { case 1: { // GU_INDEX_8BIT memoryReader = MemoryReader.getMemoryReader(ptr_index, 1 * numberOfVertex, 1); break; } case 2: { // GU_INDEX_16BIT memoryReader = MemoryReader.getMemoryReader(ptr_index, 2 * numberOfVertex, 2); break; } case 3: { // GU_INDEX_UNK3 (assume 32bit) memoryReader = MemoryReader.getMemoryReader(ptr_index, 4 * numberOfVertex, 4); break; } } // Remember the largest index int maxIndex = -1; if (memoryReader != null) { cachedIndices = new int[numberOfVertex]; for (int i = 0; i < numberOfVertex; i++) { int index = memoryReader.readNext(); cachedIndices[i] = index; if (index > maxIndex) { maxIndex = index; } } } // The vertex array extends only up to the largest index vertexArraySize = vertexSize * (maxIndex + 1); } else { vertexArraySize = vertexSize * numberOfVertex; } if (ptr_vertex != 0) { vertexArraySize = (vertexArraySize + 3) & ~3; cachedVertices = new int[vertexArraySize >> 2]; IMemoryReader verticesReader = MemoryReader.getMemoryReader(ptr_vertex, vertexArraySize, 4); for (int i = 0; i < cachedVertices.length; i++) { cachedVertices[i] = verticesReader.readNext(); } } } public void prepareForCache(VertexCache vertexCache, int numberOfVertex, float[][] boneMatrix, int numberOfWeightsForBuffer) { this.vertexCache = vertexCache; cachedNumberOfVertex = numberOfVertex; cachedMorphWeights = new float[morphingVertexCount]; System.arraycopy(morph_weight, 0, cachedMorphWeights, 0, morphingVertexCount); if (weight != 0 && numberOfWeightsForBuffer == 0 && boneMatrix != null) { cachedBoneMatrix = new float[skinningWeightCount][]; for (int i = 0; i < skinningWeightCount; i++) { cachedBoneMatrix[i] = new float[12]; System.arraycopy(boneMatrix[i], 0, cachedBoneMatrix[i], 0, 12); } } else { cachedBoneMatrix = null; } readForCache(numberOfVertex); } public void reuseCachedBuffer(VertexInfo vertexInfo) { if (vertexInfo != null && vertexInfo.cachedBuffer != null) { // Reuse the cachedBuffer if we don't have one or if we have a smaller one if (cachedBuffer == null || cachedBuffer.capacity() < vertexInfo.cachedBuffer.capacity()) { // Reuse the cachedBuffer cachedBuffer = vertexInfo.cachedBuffer; vertexInfo.cachedBuffer = null; } } } public static String toString(int texture, int color, int normal, int position, int weight, int skinningWeightCount, int morphingVertexCount, int index, boolean transform2D, int vertexSize) { StringBuilder sb = new StringBuilder(); if (texture_info[texture] != null) { sb.append(texture_info[texture] + "|"); } if (color_info[color] != null) { sb.append(color_info[color] + "|"); } if (normal_info[normal] != null) { sb.append(normal_info[normal] + "|"); } if (vertex_info[position] != null) { sb.append(vertex_info[position] + "|"); } if (weight_info[weight] != null) { sb.append(weight_info[weight] + "|"); sb.append("GU_WEIGHTS(" + skinningWeightCount + ")|"); } if (morphingVertexCount > 1) { sb.append("GU_VERTICES(" + morphingVertexCount + ")|"); } if (index_info[index] != null) { sb.append(index_info[index] + "|"); } sb.append(transform_info[transform2D ? 1 : 0]); sb.append(" size=" + vertexSize); return sb.toString(); } @Override public String toString() { return toString(texture, color, normal, position, weight, skinningWeightCount, morphingVertexCount, index, transform2D, vertexSize); } }