/*
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.RE.software;
import java.nio.Buffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.HashMap;
import jpcsp.Memory;
import jpcsp.graphics.VertexInfo;
import jpcsp.graphics.VertexState;
import jpcsp.graphics.VideoEngine;
import jpcsp.graphics.RE.IRenderingEngine;
import jpcsp.graphics.RE.NullRenderingEngine;
import jpcsp.memory.IMemoryReader;
import jpcsp.memory.ImageReader;
import jpcsp.util.DurationStatistics;
import jpcsp.util.Utilities;
/**
* @author gid15
*
* This RenderingEngine class implements a software-based rendering,
* not using OpenGL or any GPU.
* This is probably the most accurate implementation but also the slowest one.
*/
public class RESoftware extends NullRenderingEngine {
private static final boolean useTextureCache = true;
protected int genTextureId;
protected int bindTexture;
protected VertexState v1 = new VertexState();
protected VertexState v2 = new VertexState();
protected VertexState v3 = new VertexState();
protected VertexState v4 = new VertexState();
protected VertexState v5 = new VertexState();
protected VertexState v6 = new VertexState();
protected RendererExecutor rendererExecutor;
protected HashMap<Integer, CachedTextureResampled> cachedTextures = new HashMap<Integer, CachedTextureResampled>();
protected int textureBufferWidth;
protected static DurationStatistics drawArraysStatistics = new DurationStatistics("RESoftware drawArrays");
public static DurationStatistics triangleRender3DStatistics = new DurationStatistics("RESoftware TriangleRender3D");
public static DurationStatistics triangleRender2DStatistics = new DurationStatistics("RESoftware TriangleRender2D");
public static DurationStatistics spriteRenderStatistics = new DurationStatistics("RESoftware SpriteRender");
protected static DurationStatistics cachedTextureStatistics = new DurationStatistics("RESoftware CachedTexture");
public static DurationStatistics textureResamplingStatistics = new DurationStatistics("RESftware Texture resampling");
protected BoundingBoxRenderer boundingBoxRenderer;
protected boolean boundingBoxVisible;
protected BufferVertexReader bufferVertexReader;
protected boolean useVertexTexture;
public RESoftware() {
log.info("Using SoftwareRenderer");
}
@Override
public void exit() {
if (DurationStatistics.collectStatistics) {
log.info(drawArraysStatistics);
log.info(triangleRender3DStatistics);
log.info(triangleRender2DStatistics);
log.info(spriteRenderStatistics);
log.info(cachedTextureStatistics);
log.info(textureResamplingStatistics);
}
}
@Override
public void startDisplay() {
context = VideoEngine.getInstance().getContext();
rendererExecutor = RendererExecutor.getInstance();
}
@Override
public int setBones(int count, float[] values) {
return count;
}
protected void render(IRenderer renderer) {
if (renderer.prepare(context)) {
rendererExecutor.render(renderer);
}
}
protected void drawSprite(SpriteRenderer spriteRenderer, VertexState v1, VertexState v2) {
spriteRenderer.setVertex(v1, v2);
render(spriteRenderer);
}
protected CachedTextureResampled getCachedTexture() {
CachedTextureResampled cachedTexture = cachedTextures.get(bindTexture);
if (cachedTexture != null) {
cachedTexture.setClut();
}
return cachedTexture;
}
protected void drawArraysSprites(int first, int count) {
CachedTextureResampled cachedTexture = getCachedTexture();
SpriteRenderer spriteRenderer = new SpriteRenderer(context, cachedTexture, useVertexTexture);
boolean readTexture = context.textureFlag.isEnabled() && !context.clearMode;
Memory mem = Memory.getInstance();
for (int i = first; i < count - 1; i += 2) {
int addr1 = context.vinfo.getAddress(mem, i);
int addr2 = context.vinfo.getAddress(mem, i + 1);
context.vinfo.readVertex(mem, addr1, v1, readTexture, VideoEngine.getInstance().isDoubleTexture2DCoords());
context.vinfo.readVertex(mem, addr2, v2, readTexture, VideoEngine.getInstance().isDoubleTexture2DCoords());
drawSprite(spriteRenderer, v1, v2);
}
}
protected void drawTriangle(TriangleRenderer triangleRenderer, VertexState v1, VertexState v2, VertexState v3, boolean invertedFrontFace) {
triangleRenderer.setVertex(v1, v2, v3);
if (!triangleRenderer.isCulled(invertedFrontFace)) {
render(triangleRenderer);
}
}
protected boolean isSprite(VertexInfo vinfo, VertexState tv1, VertexState tv2, VertexState tv3, VertexState tv4) {
// Sprites are only available in 2D
if (!vinfo.transform2D) {
return false;
}
// Sprites are not culled. Keep triangles when the back face culling is enabled.
if (!context.clearMode && context.cullFaceFlag.isEnabled()) {
return false;
}
// Sprites have no normal
if (vinfo.normal != 0) {
return false;
}
// Color doubling not correctly handled on sprites
if (context.textureColorDoubled) {
return false;
}
if (vinfo.color != 0) {
// Color of 4 vertex must be equal
if (!Utilities.sameColor(tv1.c, tv2.c, tv3.c, tv4.c)) {
return false;
}
}
// x1 == x2 && y1 == y3 && x4 == x3 && y4 == y2
if (tv1.p[0] == tv2.p[0] && tv1.p[1] == tv3.p[1] && tv4.p[0] == tv3.p[0] && tv4.p[1] == tv2.p[1]) {
// z1 == z2 && z1 == z3 && z1 == z4
if (tv1.p[2] == tv2.p[2] && tv1.p[2] == tv3.p[2] && tv1.p[2] == tv3.p[2]) {
if (vinfo.texture == 0) {
return true;
}
// u1 == u2 && v1 == v3 && u4 == u3 && v4 == v2
if (tv1.t[0] == tv2.t[0] && tv1.t[1] == tv3.t[1] && tv4.t[0] == tv3.t[0] && tv4.t[1] == tv2.t[1]) {
return true;
}
// v1 == v2 && u1 == u3 && v4 == v3 && u4 == u2
// if (tv1.t[1] == tv2.t[1] && tv1.t[0] == tv3.t[0] && tv4.t[1] == tv3.t[1] && tv4.t[0] == tv2.t[0]) {
// return true;
// }
}
}
// y1 == y2 && x1 == x3 && y4 == y3 && x4 == x2
if (tv1.p[1] == tv2.p[1] && tv1.p[0] == tv3.p[0] && tv4.p[1] == tv3.p[1] && tv4.p[0] == tv2.p[0]) {
// z1 == z2 && z1 == z3 && z1 == z4
if (tv1.p[2] == tv2.p[2] && tv1.p[2] == tv3.p[2] && tv1.p[2] == tv3.p[2]) {
if (vinfo.texture == 0) {
return true;
}
// v1 == v2 && u1 == u3 && v4 == v3 && u4 == u2
if (tv1.t[1] == tv2.t[1] && tv1.t[0] == tv3.t[0] && tv4.t[1] == tv3.t[1] && tv4.t[0] == tv2.t[0]) {
return true;
}
// u1 == u2 && v1 == v3 && u4 == u3 && v4 == v2
// if (tv1.t[0] == tv2.t[0] && tv1.t[1] == tv3.t[1] && tv4.t[0] == tv3.t[0] && tv4.t[1] == tv2.t[1]) {
// return true;
// }
}
}
return false;
}
protected void resetBufferVertexReader() {
bufferVertexReader = null;
}
protected void readVertex(Memory mem, int index, VertexState v, boolean readTexture) {
if (bufferVertexReader == null) {
int addr = context.vinfo.getAddress(mem, index);
context.vinfo.readVertex(mem, addr, v, readTexture, VideoEngine.getInstance().isDoubleTexture2DCoords());
} else {
// This is used for spline and bezier curves:
// the VideoEngine is computing the vertices and is pushing them into a buffer.
bufferVertexReader.readVertex(index, v);
}
if (context.vinfo.weight != 0) {
VideoEngine.doSkinning(context.bone_uploaded_matrix, context.vinfo, v);
}
}
protected void drawArraysTriangleStrips(int first, int count) {
Memory mem = Memory.getInstance();
CachedTextureResampled cachedTexture = getCachedTexture();
TriangleRenderer triangleRenderer = new TriangleRenderer(context, cachedTexture, useVertexTexture);
SpriteRenderer spriteRenderer = null;
VertexState tv1 = null;
VertexState tv2 = null;
VertexState tv3 = null;
VertexState tv4 = v1;
boolean readTexture = context.textureFlag.isEnabled() && !context.clearMode;
for (int i = 0; i < count; i++) {
readVertex(mem, first + i, tv4, readTexture);
if (tv3 != null) {
// Displaying a sprite (i.e. rectangular area) is faster.
// Try to merge adjacent triangles if they form a sprite.
if (isSprite(context.vinfo, tv1, tv2, tv3, tv4)) {
if (spriteRenderer == null) {
spriteRenderer = new SpriteRenderer(context, cachedTexture, useVertexTexture);
}
drawSprite(spriteRenderer, tv1, tv4);
v5.copy(tv3);
v6.copy(tv4);
v1.copy(v5);
v2.copy(v6);
tv1 = v1;
tv2 = v2;
tv3 = null;
tv4 = v3;
} else {
// The Front face direction is inverted every 2 triangles in the strip.
drawTriangle(triangleRenderer, tv1, tv2, tv3, ((i - 3) & 1) != 0);
VertexState v = tv1;
tv1 = tv2;
tv2 = tv3;
tv3 = tv4;
tv4 = v;
}
} else if (tv1 == null) {
tv1 = tv4;
tv4 = v2;
} else if (tv2 == null) {
tv2 = tv4;
tv4 = v3;
} else {
tv3 = tv4;
tv4 = v4;
}
}
if (tv3 != null) {
// The Front face direction is inverted every 2 triangles in the strip.
drawTriangle(triangleRenderer, tv1, tv2, tv3, (count & 1) == 0);
}
}
protected void drawArraysTriangles(int first, int count) {
Memory mem = Memory.getInstance();
CachedTextureResampled cachedTexture = getCachedTexture();
TriangleRenderer triangleRenderer = new TriangleRenderer(context, cachedTexture, useVertexTexture);
boolean readTexture = context.textureFlag.isEnabled() && !context.clearMode;
for (int i = 0; i < count; i += 3) {
readVertex(mem, first + i, v1, readTexture);
readVertex(mem, first + i + 1, v2, readTexture);
readVertex(mem, first + i + 2, v3, readTexture);
drawTriangle(triangleRenderer, v1, v2, v3, false);
}
}
protected void drawArraysTriangleFan(int first, int count) {
Memory mem = Memory.getInstance();
CachedTextureResampled cachedTexture = getCachedTexture();
TriangleRenderer triangleRenderer = new TriangleRenderer(context, cachedTexture, useVertexTexture);
VertexState tv1 = null;
VertexState tv2 = null;
VertexState tv3 = v1;
boolean readTexture = context.textureFlag.isEnabled() && !context.clearMode;
for (int i = 0; i < count; i++) {
readVertex(mem, first + i, tv3, readTexture);
if (tv2 != null) {
drawTriangle(triangleRenderer, tv1, tv2, tv3, false);
VertexState v = tv2;
tv2 = tv3;
tv3 = v;
} else if (tv1 == null) {
tv1 = tv3;
tv3 = v2;
} else {
tv2 = tv3;
tv3 = v3;
}
}
}
@Override
public void drawArrays(int primitive, int first, int count) {
drawArraysStatistics.start();
switch (primitive) {
case IRenderingEngine.GU_SPRITES:
drawArraysSprites(first, count);
break;
case IRenderingEngine.GU_TRIANGLE_STRIP:
drawArraysTriangleStrips(first, count);
break;
case IRenderingEngine.GU_TRIANGLES:
drawArraysTriangles(first, count);
break;
case IRenderingEngine.GU_TRIANGLE_FAN:
drawArraysTriangleFan(first, count);
break;
}
drawArraysStatistics.end();
}
@Override
public void setTexCoordPointer(int size, int type, int stride, int bufferSize, Buffer buffer) {
if (bufferVertexReader == null) {
bufferVertexReader = new BufferVertexReader();
}
bufferVertexReader.setTextureComponentInfo(size, type, stride, bufferSize, buffer);
}
@Override
public void setColorPointer(int size, int type, int stride, int bufferSize, Buffer buffer) {
if (bufferVertexReader == null) {
bufferVertexReader = new BufferVertexReader();
}
bufferVertexReader.setColorComponentInfo(size, type, stride, bufferSize, buffer);
}
@Override
public void setVertexPointer(int size, int type, int stride, int bufferSize, Buffer buffer) {
if (bufferVertexReader == null) {
bufferVertexReader = new BufferVertexReader();
}
bufferVertexReader.setVertexComponentInfo(size, type, stride, bufferSize, buffer);
}
@Override
public void setNormalPointer(int type, int stride, int bufferSize, Buffer buffer) {
if (bufferVertexReader == null) {
bufferVertexReader = new BufferVertexReader();
}
bufferVertexReader.setNormalComponentInfo(type, stride, bufferSize, buffer);
}
@Override
public void setWeightPointer(int size, int type, int stride, int bufferSize, Buffer buffer) {
if (bufferVertexReader == null) {
bufferVertexReader = new BufferVertexReader();
}
bufferVertexReader.setWeightComponentInfo(size, type, stride, bufferSize, buffer);
}
@Override
public void setPixelStore(int rowLength, int alignment) {
textureBufferWidth = rowLength;
}
@Override
public int genTexture() {
return genTextureId++;
}
@Override
public void bindTexture(int texture) {
bindTexture = texture;
}
@Override
public void deleteTexture(int texture) {
cachedTextures.remove(texture);
}
@Override
public void setCompressedTexImage(int level, int internalFormat, int width, int height, int compressedSize, Buffer buffer) {
if (useTextureCache) {
cachedTextureStatistics.start();
// TODO Cache all the texture levels
if (level == 0) {
int bufferWidth = context.texture_buffer_width[level];
IMemoryReader imageReader = ImageReader.getImageReader(context.texture_base_pointer[level], width, height, bufferWidth, internalFormat, false, 0, 0, 0, 0, 0, 0, null, null);
CachedTexture cachedTexture = CachedTexture.getCachedTexture(Math.min(width, bufferWidth), height, internalFormat, imageReader);
CachedTextureResampled cachedTextureResampled = new CachedTextureResampled(cachedTexture);
cachedTextures.put(bindTexture, cachedTextureResampled);
}
cachedTextureStatistics.end();
}
}
@Override
public void setTexImage(int level, int internalFormat, int width, int height, int format, int type, int textureSize, Buffer buffer) {
if (useTextureCache) {
cachedTextureStatistics.start();
// TODO Cache all the texture levels
if (level == 0) {
CachedTexture cachedTexture = null;
if (buffer instanceof IntBuffer) {
cachedTexture = CachedTexture.getCachedTexture(textureBufferWidth, height, format, ((IntBuffer) buffer).array(), buffer.arrayOffset(), textureSize >> 2);
} else if (buffer instanceof ShortBuffer) {
cachedTexture = CachedTexture.getCachedTexture(textureBufferWidth, height, format, ((ShortBuffer) buffer).array(), buffer.arrayOffset(), textureSize >> 1);
}
CachedTextureResampled cachedTextureResampled = new CachedTextureResampled(cachedTexture);
cachedTextures.put(bindTexture, cachedTextureResampled);
}
cachedTextureStatistics.end();
}
}
@Override
public void beginBoundingBox(int numberOfVertexBoundingBox) {
boundingBoxRenderer = new BoundingBoxRenderer(context);
boundingBoxVisible = true;
}
@Override
public void drawBoundingBox(float[][] values) {
if (boundingBoxVisible) {
boundingBoxRenderer.drawBoundingBox(values);
if (!boundingBoxRenderer.prepare(context)) {
boundingBoxVisible = false;
}
}
}
@Override
public void endBoundingBox(VertexInfo vinfo) {
}
@Override
public boolean isBoundingBoxVisible() {
return boundingBoxVisible;
}
@Override
public boolean canAllNativeVertexInfo() {
return true;
}
@Override
public boolean canNativeSpritesPrimitive() {
return true;
}
@Override
public void setVertexInfo(VertexInfo vinfo, boolean allNativeVertexInfo, boolean useVertexColor, boolean useTexture, int type) {
this.useVertexTexture = useTexture;
resetBufferVertexReader();
}
@Override
public boolean canNativeClut(int textureAddress, boolean textureSwizzle) {
if (Memory.isVRAM(textureAddress) && !textureSwizzle) {
return true;
}
return !useTextureCache;
}
@Override
public void waitForRenderingCompletion() {
rendererExecutor.waitForRenderingCompletion();
}
@Override
public boolean canReadAllVertexInfo() {
// drawArrays doesn't need vertex infos in buffers, it can read directly from memory.
return true;
}
@Override
public boolean setCopyRedToAlpha(boolean copyRedToAlpha) {
return true;
}
@Override
public void setVertexColor(float[] color) {
for (int i = 0; i < context.vertexColor.length; i++) {
context.vertexColor[i] = color[i];
}
}
}