vtm/src/org/oscim/view/renderer/GLRenderer.java
Hannes Janetzek a1317a9de5 - remove swrenderer
- rearchitect:
 now that MapView is a ViewGroup and MapRenderer is the GLSurfaceView.
- lock/unlock proxy tiles properly to not be removed from cache while in use
2013-10-09 01:22:33 +02:00

833 lines
21 KiB
Java

/*
* Copyright 2012 Hannes Janetzek
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program 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 Lesser General License for more details.
*
* You should have received a copy of the GNU Lesser General License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.view.renderer;
import static android.opengl.GLES20.GL_ARRAY_BUFFER;
import static android.opengl.GLES20.GL_BLEND;
import static android.opengl.GLES20.GL_DYNAMIC_DRAW;
import static android.opengl.GLES20.GL_ONE_MINUS_SRC_ALPHA;
import static android.opengl.GLES20.GL_STENCIL_BUFFER_BIT;
import static android.opengl.GLES20.glBindBuffer;
import static android.opengl.GLES20.glBlendFunc;
import static android.opengl.GLES20.glBufferData;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glClearStencil;
import static android.opengl.GLES20.glDisable;
import static android.opengl.GLES20.glEnable;
import static android.opengl.GLES20.glGenBuffers;
import static android.opengl.GLES20.glViewport;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import org.oscim.core.MapPosition;
import org.oscim.core.Tile;
import org.oscim.theme.RenderTheme;
import org.oscim.utils.GlUtils;
import org.oscim.view.MapView;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.os.SystemClock;
import android.util.FloatMath;
import android.util.Log;
public class GLRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "SurfaceRenderer";
private static final int MB = 1024 * 1024;
private static final int SHORT_BYTES = 2;
static final float COORD_MULTIPLIER = 8.0f;
private static final int CACHE_TILES_MAX = 200;
private static final int LIMIT_BUFFERS = 16 * MB;
static int CACHE_TILES = CACHE_TILES_MAX;
private final MapView mMapView;
private static ArrayList<VertexBufferObject> mVBOs;
private static int mWidth, mHeight;
private static float mAspect;
private static int rotateBuffers = 2;
private static ShortBuffer shortBuffer[];
private static short[] mFillCoords;
// bytes currently loaded in VBOs
private static int mBufferMemoryUsage;
private static float[] mMVPMatrix = new float[16];
private static float[] mRotateMatrix = new float[16];
private static float[] mProjMatrix = new float[16];
private static float[] mRotTMatrix = new float[16];
// curTiles is set by TileLoader and swapped with
// drawTiles in onDrawFrame in GL thread.
private static TilesData curTiles, drawTiles;
// flag set by updateVisibleList when current visible tiles
// changed. used in onDrawFrame to flip curTiles/drawTiles
private static boolean mUpdateTiles;
private static MapPosition mCurPosition;
private float[] mClearColor = null;
// number of tiles drawn in one frame
private static short mDrawCount = 0;
private static boolean mUpdateColor = false;
// lock to synchronize Main- and GL-Thread
static Object lock = new Object();
// used for passing tiles to be rendered from TileLoader(Main-Thread) to GLThread
static class TilesData {
int cnt = 0;
final MapTile[] tiles;
TilesData(int numTiles) {
tiles = new MapTile[numTiles];
}
}
/**
* @param mapView
* the MapView
*/
public GLRenderer(MapView mapView) {
Log.d(TAG, "init MapRenderer");
mMapView = mapView;
Matrix.setIdentityM(mMVPMatrix, 0);
mUpdateTiles = false;
}
/**
* called by TileLoader when only position changed
*
* @param mapPosition
* current MapPosition
*/
static void updatePosition(MapPosition mapPosition) {
mCurPosition = mapPosition;
}
/**
* called by TileLoader when list of active tiles changed
*
* @param mapPosition
* current MapPosition
* @param tiles
* active tiles
* @return curTiles (the previously active tiles)
*/
static TilesData updateTiles(MapPosition mapPosition, TilesData tiles) {
synchronized (GLRenderer.lock) {
mCurPosition = mapPosition;
// unlock previously active tiles
for (int i = 0; i < curTiles.cnt; i++) {
MapTile t = curTiles.tiles[i];
boolean found = false;
for (int j = 0; j < tiles.cnt; j++) {
if (tiles.tiles[j] == t) {
found = true;
break;
}
}
if (found)
continue;
for (int j = 0; j < drawTiles.cnt; j++) {
if (drawTiles.tiles[j] == t) {
found = true;
break;
}
}
if (found)
continue;
t.unlock();
}
TilesData tmp = curTiles;
curTiles = tiles;
// lock tiles (and their proxies) to not be removed from cache
for (int i = 0; i < curTiles.cnt; i++) {
MapTile t = curTiles.tiles[i];
if (!t.isActive)
t.lock();
}
for (int j = 0; j < drawTiles.cnt; j++) {
MapTile t = drawTiles.tiles[j];
if (!t.isActive)
t.lock();
}
mUpdateTiles = true;
return tmp;
}
}
/**
* called by TileLoader. when tile is removed from cache, reuse its vbo.
*
* @param vbo
* the VBO
*/
static void addVBO(VertexBufferObject vbo) {
synchronized (mVBOs) {
mVBOs.add(vbo);
}
}
void setVBO(MapTile tile) {
synchronized (mVBOs) {
int numVBOs = mVBOs.size();
if (numVBOs > 0 && tile.vbo == null) {
tile.vbo = mVBOs.remove(numVBOs - 1);
}
}
}
void setRenderTheme(RenderTheme t) {
int bg = t.getMapBackground();
float[] c = new float[4];
c[3] = (bg >> 24 & 0xff) / 255.0f;
c[0] = (bg >> 16 & 0xff) / 255.0f;
c[1] = (bg >> 8 & 0xff) / 255.0f;
c[2] = (bg >> 0 & 0xff) / 255.0f;
mClearColor = c;
mUpdateColor = true;
}
// depthRange: -1 to 1, bits: 2^24 => 2/2^24 one step
// ... asus has just 16 bit?!
// private static final float depthStep = 0.00000011920928955078125f;
private static boolean mRotate = false;
private static void setMatrix(MapPosition mapPosition, MapTile tile, float div,
int offset) {
float x, y, scale;
if (mRotate) {
scale = (float) (1.0 * mapPosition.scale / (mHeight * div));
x = (float) (tile.pixelX - mapPosition.x * div);
y = (float) (tile.pixelY - mapPosition.y * div);
Matrix.setIdentityM(mMVPMatrix, 0);
Matrix.scaleM(mMVPMatrix, 0, scale / COORD_MULTIPLIER,
scale / COORD_MULTIPLIER, 1);
Matrix.translateM(mMVPMatrix, 0,
x * COORD_MULTIPLIER,
-(y + Tile.TILE_SIZE) * COORD_MULTIPLIER,
-0.99f + offset * 0.01f);
Matrix.multiplyMM(mMVPMatrix, 0, mRotateMatrix, 0, mMVPMatrix, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
}
else {
scale = (float) (2.0 * mapPosition.scale / (mHeight * div));
x = (float) (tile.pixelX - mapPosition.x * div);
y = (float) (tile.pixelY - mapPosition.y * div);
mMVPMatrix[12] = x * scale * mAspect;
mMVPMatrix[13] = -(y + Tile.TILE_SIZE) * scale;
// increase the 'distance' with each tile drawn.
mMVPMatrix[14] = -0.99f + offset * 0.01f; // depthStep; // 0.01f;
mMVPMatrix[0] = scale * mAspect / COORD_MULTIPLIER;
mMVPMatrix[5] = scale / COORD_MULTIPLIER;
}
}
private static boolean isVisible(MapPosition mapPosition, MapTile tile, float div) {
double dx, dy, scale;
if (div == 0) {
dx = tile.pixelX - mapPosition.x;
dy = tile.pixelY - mapPosition.y;
scale = mapPosition.scale;
} else {
dx = tile.pixelX - mapPosition.x * div;
dy = tile.pixelY - mapPosition.y * div;
scale = mapPosition.scale / div;
}
int size = Tile.TILE_SIZE;
int sx = (int) (dx * scale);
int sy = (int) (dy * scale);
// FIXME little hack, need to do scanline check or sth
// this kindof works for typical screen aspect
if (mRotate) {
int ssize = mWidth > mHeight ? mWidth : mHeight;
if (sy > ssize / 2 || sx > ssize / 2
|| sx + size * scale < -ssize / 2
|| sy + size * scale < -ssize / 2) {
tile.isVisible = false;
return false;
}
} else {
if (sy > mHeight / 2 || sx > mWidth / 2
|| sx + size * scale < -mWidth / 2
|| sy + size * scale < -mHeight / 2) {
tile.isVisible = false;
return false;
}
}
tile.isVisible = true;
return true;
}
private int uploadCnt = 0;
private boolean uploadTileData(MapTile tile) {
ShortBuffer sbuf = null;
// use multiple buffers to avoid overwriting buffer while current
// data is uploaded (or rather the blocking which is probably done to avoid this)
if (uploadCnt >= rotateBuffers) {
uploadCnt = 0;
GLES20.glFlush();
}
// Upload line data to vertex buffer object
synchronized (tile) {
if (!tile.newData)
return false;
int lineSize = LineRenderer.sizeOf(tile.lineLayers);
int polySize = PolygonRenderer.sizeOf(tile.polygonLayers);
int newSize = lineSize + polySize;
if (newSize == 0) {
LineRenderer.clear(tile.lineLayers);
PolygonRenderer.clear(tile.polygonLayers);
tile.lineLayers = null;
tile.polygonLayers = null;
tile.newData = false;
return false;
}
// Log.d(TAG, "uploadTileData, " + tile);
glBindBuffer(GL_ARRAY_BUFFER, tile.vbo.id);
sbuf = shortBuffer[uploadCnt];
// add fill coordinates
newSize += 8;
// FIXME probably not a good idea to do this in gl thread...
if (sbuf == null || sbuf.capacity() < newSize) {
ByteBuffer bbuf = ByteBuffer.allocateDirect(newSize * SHORT_BYTES).order(
ByteOrder.nativeOrder());
sbuf = bbuf.asShortBuffer();
shortBuffer[uploadCnt] = sbuf;
sbuf.put(mFillCoords, 0, 8);
}
sbuf.clear();
sbuf.position(8);
PolygonRenderer.compileLayerData(tile.polygonLayers, sbuf);
tile.lineOffset = (8 + polySize);
if (tile.lineOffset != sbuf.position())
Log.d(TAG, "tiles lineoffset is wrong: " + tile + " "
+ tile.lineOffset + " "
+ sbuf.position() + " "
+ sbuf.limit() + " "
+ sbuf.remaining() + " "
+ PolygonRenderer.sizeOf(tile.polygonLayers) + " "
+ tile.rel);
tile.lineOffset *= SHORT_BYTES;
LineRenderer.compileLayerData(tile.lineLayers, sbuf);
sbuf.flip();
if (newSize != sbuf.remaining()) {
Log.d(TAG, "tiles wrong: " + tile + " "
+ newSize + " "
+ sbuf.position() + " "
+ sbuf.limit() + " "
+ sbuf.remaining() + " "
+ LineRenderer.sizeOf(tile.lineLayers)
+ tile.isLoading + " "
+ tile.rel);
tile.newData = false;
return false;
}
newSize *= SHORT_BYTES;
if (tile.vbo.size > newSize && tile.vbo.size < newSize * 4
&& mBufferMemoryUsage < LIMIT_BUFFERS) {
GLES20.glBufferSubData(GL_ARRAY_BUFFER, 0, newSize, sbuf);
} else {
mBufferMemoryUsage -= tile.vbo.size;
tile.vbo.size = newSize;
glBufferData(GL_ARRAY_BUFFER, tile.vbo.size, sbuf, GL_DYNAMIC_DRAW);
mBufferMemoryUsage += tile.vbo.size;
}
uploadCnt++;
tile.isReady = true;
tile.newData = false;
}
return true;
}
private static void checkBufferUsage() {
// try to clear some unused vbo when exceding limit
if (mBufferMemoryUsage > LIMIT_BUFFERS) {
Log.d(TAG, "buffer object usage: " + mBufferMemoryUsage / MB + "MB");
glBindBuffer(GL_ARRAY_BUFFER, 0);
int buf[] = new int[1];
synchronized (mVBOs) {
for (VertexBufferObject vbo : mVBOs) {
if (vbo.size == 0)
continue;
mBufferMemoryUsage -= vbo.size;
// this should free allocated memory but it does not.
// on HTC it causes oom exception?!
// glBindBuffer(GL_ARRAY_BUFFER, vbo.id);
// glBufferData(GL_ARRAY_BUFFER, 0, null, GLES20.GL_STATIC_DRAW);
// recreate vbo instead
buf[0] = vbo.id;
GLES20.glDeleteBuffers(1, buf, 0);
GLES20.glGenBuffers(1, buf, 0);
vbo.id = buf[0];
vbo.size = 0;
}
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
Log.d(TAG, " > " + mBufferMemoryUsage / MB + "MB");
if (mBufferMemoryUsage > LIMIT_BUFFERS && CACHE_TILES > 100)
CACHE_TILES -= 50;
} else if (CACHE_TILES < CACHE_TILES_MAX) {
CACHE_TILES += 50;
}
}
@Override
public void onDrawFrame(GL10 glUnused) {
long start = 0;
MapPosition mapPosition;
if (MapView.debugFrameTime)
start = SystemClock.uptimeMillis();
if (mRotate != (mMapView.enableRotation || mMapView.enableCompass)) {
Matrix.setIdentityM(mMVPMatrix, 0);
mRotate = mMapView.enableRotation || mMapView.enableCompass;
}
if (mUpdateColor && mClearColor != null) {
glClearColor(mClearColor[0], mClearColor[1], mClearColor[2], mClearColor[3]);
mUpdateColor = false;
}
GLES20.glDepthMask(true);
// Note: having the impression it is faster to also clear the
// stencil buffer even when not needed. probaly otherwise it
// is masked out from the depth buffer as they share the same
// memory region afaik
glClear(GLES20.GL_COLOR_BUFFER_BIT
| GLES20.GL_DEPTH_BUFFER_BIT
| GLES20.GL_STENCIL_BUFFER_BIT);
// get position and current tiles to draw
synchronized (GLRenderer.lock) {
mapPosition = mCurPosition;
if (mUpdateTiles) {
TilesData tmp = drawTiles;
drawTiles = curTiles;
curTiles = tmp;
mUpdateTiles = false;
}
}
if (drawTiles == null)
return;
int tileCnt = drawTiles.cnt;
MapTile[] tiles = drawTiles.tiles;
uploadCnt = 0;
int updateTextures = 0;
// check visible tiles, upload new vertex data
for (int i = 0; i < tileCnt; i++) {
MapTile tile = tiles[i];
if (!isVisible(mapPosition, tile, 1))
continue;
if (tile.texture == null && TextRenderer.drawToTexture(tile))
updateTextures++;
if (tile.newData) {
uploadTileData(tile);
continue;
}
if (!tile.isReady) {
// check near relatives if they can serve as proxy
MapTile rel = tile.rel.parent.tile;
if (rel != null && rel.newData) {
uploadTileData(rel);
} else {
for (int c = 0; c < 4; c++) {
if (tile.rel.child[c] == null)
continue;
rel = tile.rel.child[c].tile;
if (rel != null && rel.newData)
uploadTileData(rel);
}
}
}
}
if (uploadCnt > 0)
checkBufferUsage();
if (updateTextures > 0)
TextRenderer.compileTextures();
if (mRotate) {
Matrix.setRotateM(mRotateMatrix, 0, mapPosition.angle, 0, 0, 1);
Matrix.transposeM(mRotTMatrix, 0, mRotateMatrix, 0);
} else {
Matrix.setIdentityM(mRotTMatrix, 0);
}
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
for (int i = 0; i < tileCnt; i++) {
if (tiles[i].isVisible && tiles[i].isReady) {
drawTile(mapPosition, tiles[i], 1);
}
}
// proxies are clipped to the region where nothing was drawn to depth buffer
// TODO draw all parent before grandparent
for (int i = 0; i < tileCnt; i++) {
if (tiles[i].isVisible && !tiles[i].isReady) {
drawProxyTile(mapPosition, tiles[i]);
}
}
// GlUtils.checkGlError("end draw");
glDisable(GLES20.GL_DEPTH_TEST);
mDrawCount = 0;
mDrawSerial++;
glEnable(GL_BLEND);
int z = mapPosition.zoomLevel;
float s = mapPosition.scale;
int zoomLevelDiff = Math.max(z - MapGenerator.STROKE_MAX_ZOOM_LEVEL, 0);
float scale = (float) Math.pow(1.4, zoomLevelDiff);
if (scale < 1)
scale = 1;
if (z >= MapGenerator.STROKE_MAX_ZOOM_LEVEL)
TextRenderer.beginDraw(FloatMath.sqrt(s) / scale, mRotTMatrix);
else
TextRenderer.beginDraw(s, mRotTMatrix);
for (int i = 0; i < tileCnt; i++) {
if (!tiles[i].isVisible || tiles[i].texture == null)
continue;
setMatrix(mapPosition, tiles[i], 1, 0);
TextRenderer.drawTile(tiles[i], mMVPMatrix);
}
TextRenderer.endDraw();
if (MapView.debugFrameTime) {
GLES20.glFinish();
Log.d(TAG, "draw took " + (SystemClock.uptimeMillis() - start));
}
}
// used to not draw a tile twice per frame...
private static byte mDrawSerial = 0;
private static void drawTile(MapPosition mapPosition, MapTile tile, float div) {
// draw parents only once
if (tile.lastDraw == mDrawSerial)
return;
tile.lastDraw = mDrawSerial;
int z = mapPosition.zoomLevel;
float s = mapPosition.scale;
// mDrawCount is used to calculation z offset.
// (used for depth clipping)
setMatrix(mapPosition, tile, div, mDrawCount++);
glBindBuffer(GL_ARRAY_BUFFER, tile.vbo.id);
LineLayer ll = tile.lineLayers;
PolygonLayer pl = tile.polygonLayers;
boolean clipped = false;
for (; pl != null || ll != null;) {
int lnext = Integer.MAX_VALUE;
int pnext = Integer.MAX_VALUE;
if (ll != null)
lnext = ll.layer;
if (pl != null)
pnext = pl.layer;
if (pl != null && pnext < lnext) {
glDisable(GL_BLEND);
pl = PolygonRenderer.drawPolygons(pl, lnext, mMVPMatrix, z, s, !clipped);
clipped = true;
} else {
// XXX nasty
if (!clipped) {
PolygonRenderer.drawPolygons(null, 0, mMVPMatrix, z, s, true);
clipped = true;
}
glEnable(GL_BLEND);
ll = LineRenderer.drawLines(tile, ll, pnext, mMVPMatrix, div, z, s);
}
}
}
// TODO could use tile.proxies here
private static boolean drawProxyChild(MapPosition mapPosition, MapTile tile) {
int drawn = 0;
for (int i = 0; i < 4; i++) {
if (tile.rel.child[i] == null)
continue;
MapTile c = tile.rel.child[i].tile;
if (c == null)
continue;
if (!isVisible(mapPosition, c, 2)) {
drawn++;
continue;
}
if (c.isReady) {
drawTile(mapPosition, c, 2);
drawn++;
}
}
return drawn == 4;
}
// TODO could use tile.proxies here
private static void drawProxyTile(MapPosition mapPosition, MapTile tile) {
if (mapPosition.scale > 1.5f) {
// prefer drawing children
if (!drawProxyChild(mapPosition, tile)) {
MapTile t = tile.rel.parent.tile;
if (t != null) {
if (t.isReady) {
drawTile(mapPosition, t, 0.5f);
} else {
MapTile p = t.rel.parent.tile;
if (p != null && p.isReady)
drawTile(mapPosition, p, 0.25f);
}
}
}
} else {
// prefer drawing parent
MapTile t = tile.rel.parent.tile;
if (t != null && t.isReady) {
drawTile(mapPosition, t, 0.5f);
} else if (!drawProxyChild(mapPosition, tile)) {
// need to check rel.parent here, t could alread be root
if (t != null) {
t = t.rel.parent.tile;
if (t != null && t.isReady)
drawTile(mapPosition, t, 0.25f);
}
}
}
}
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
Log.d(TAG, "SurfaceChanged:" + width + " " + height);
mWidth = width;
mHeight = height;
if (width <= 0 || height <= 0)
return;
boolean changed = true;
if (mWidth == width || mHeight == height)
changed = false;
mAspect = (float) height / width;
Matrix.orthoM(mProjMatrix, 0, -0.5f / mAspect, 0.5f / mAspect, -0.5f, 0.5f, -1, 1);
// Matrix.frustumM(mProjMatrix, 0, -0.5f / mAspect, 0.5f / mAspect, -0.5f, 0.7f,
// 1, 100);
glViewport(0, 0, width, height);
if (!changed && !mNewSurface) {
mMapView.redrawMap();
return;
}
mBufferMemoryUsage = 0;
int numTiles = (mWidth / (Tile.TILE_SIZE / 2) + 2)
* (mHeight / (Tile.TILE_SIZE / 2) + 2);
// Set up vertex buffer objects
int numVBO = (CACHE_TILES + (numTiles * 2));
int[] mVboIds = new int[numVBO];
glGenBuffers(numVBO, mVboIds, 0);
GlUtils.checkGlError("glGenBuffers");
mVBOs = new ArrayList<VertexBufferObject>(numVBO);
for (int i = 1; i < numVBO; i++)
mVBOs.add(new VertexBufferObject(mVboIds[i]));
// Set up textures
TextRenderer.init(numTiles);
if (mClearColor != null) {
glClearColor(mClearColor[0], mClearColor[1],
mClearColor[2], mClearColor[3]);
} else {
glClearColor(0.98f, 0.98f, 0.97f, 1.0f);
}
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
// glEnable(GL_SCISSOR_TEST);
// glScissor(0, 0, mWidth, mHeight);
glDisable(GLES20.GL_CULL_FACE);
glBlendFunc(GLES20.GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
GlUtils.checkGlError("onSurfaceChanged");
mMapView.redrawMap();
}
void clearTiles() {
int numTiles = (mWidth / (Tile.TILE_SIZE / 2) + 2)
* (mHeight / (Tile.TILE_SIZE / 2) + 2);
Log.d(TAG, "clearTiles " + numTiles);
drawTiles = new TilesData(numTiles);
curTiles = new TilesData(numTiles);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
String ext = GLES20.glGetString(GLES20.GL_EXTENSIONS);
Log.d(TAG, "Extensions: " + ext);
shortBuffer = new ShortBuffer[rotateBuffers];
// add half pixel to tile clip/fill coordinates
// to avoid rounding issues
short min = -4;
short max = (short) ((Tile.TILE_SIZE << 3) + 4);
mFillCoords = new short[8];
mFillCoords[0] = min;
mFillCoords[1] = max;
mFillCoords[2] = max;
mFillCoords[3] = max;
mFillCoords[4] = min;
mFillCoords[5] = min;
mFillCoords[6] = max;
mFillCoords[7] = min;
LineRenderer.init();
PolygonRenderer.init();
mNewSurface = true;
}
private boolean mNewSurface;
}