/*
* Copyright 2012, 2013 OpenScienceMap
*
* 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 .
*/
package org.oscim.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;
import static android.opengl.GLES20.GL_ONE_MINUS_SRC_ALPHA;
import static org.oscim.generator.JobTile.STATE_NEW_DATA;
import static org.oscim.generator.JobTile.STATE_READY;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
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.renderer.layer.Layers;
import org.oscim.renderer.overlays.RenderOverlay;
import org.oscim.theme.RenderTheme;
import org.oscim.utils.FastMath;
import org.oscim.utils.GlUtils;
import org.oscim.view.MapView;
import org.oscim.view.MapViewPosition;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.os.SystemClock;
import android.util.Log;
/**
* @author Hannes Janetzek
*/
public class GLRenderer implements GLSurfaceView.Renderer {
private static final String TAG = GLRenderer.class.getName();
private static final int MB = 1024 * 1024;
private static final int SHORT_BYTES = 2;
private static final int CACHE_TILES_MAX = 250;
private static final int LIMIT_BUFFERS = 16 * MB;
public static final float COORD_MULTIPLIER = 8.0f;
static int CACHE_TILES = CACHE_TILES_MAX;
private static MapView mMapView;
static int mWidth, mHeight;
private static MapViewPosition mMapViewPosition;
private static MapPosition mMapPosition;
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[] mProjMatrix = new float[16];
private static float[] mTmpMatrix = new float[16];
private static float[] mTileCoords = new float[8];
private static float[] mDebugCoords = new float[8];
private static float[] mClearColor = null;
private static boolean mUpdateColor = false;
// drawlock to synchronize Main- and GL-Thread
// static ReentrantLock tilelock = new ReentrantLock();
static ReentrantLock drawlock = new ReentrantLock();
// Add additional tiles that serve as placeholer when flipping
// over date-line.
// I dont really like this but cannot think of a better solution:
// the other option would be to run scanbox each time for upload,
// drawing, proxies and text layer. needing to add placeholder only
// happens rarely, unless you live on Fidschi
/* package */static int mHolderCount;
/* package */static TileSet mDrawTiles;
// scanline fill class used to check tile visibility
private static ScanBox mScanBox = new ScanBox() {
@Override
void setVisible(int y, int x1, int x2) {
int cnt = mDrawTiles.cnt;
MapTile[] tiles = mDrawTiles.tiles;
for (int i = 0; i < cnt; i++) {
MapTile t = tiles[i];
if (t.tileY == y && t.tileX >= x1 && t.tileX < x2)
t.isVisible = true;
}
int xmax = 1 << mZoom;
if (x1 >= 0 && x2 < xmax)
return;
// add placeholder tiles to show both sides
// of date line. a little too complicated...
for (int x = x1; x < x2; x++) {
MapTile holder = null;
MapTile tile = null;
boolean found = false;
if (x >= 0 && x < xmax)
continue;
int xx = x;
if (x < 0)
xx = xmax + x;
else
xx = x - xmax;
if (xx < 0 || xx >= xmax)
continue;
for (int i = cnt; i < cnt + mHolderCount; i++)
if (tiles[i].tileX == x && tiles[i].tileY == y) {
found = true;
break;
}
if (found)
continue;
for (int i = 0; i < cnt; i++)
if (tiles[i].tileX == xx && tiles[i].tileY == y) {
tile = tiles[i];
break;
}
if (tile == null)
continue;
holder = new MapTile(x, y, mZoom);
holder.isVisible = true;
holder.holder = tile;
tile.isVisible = true;
tiles[cnt + mHolderCount++] = holder;
}
}
};
/**
* @param mapView
* the MapView
*/
public GLRenderer(MapView mapView) {
mMapView = mapView;
mMapViewPosition = mapView.getMapViewPosition();
mMapPosition = new MapPosition();
mMapPosition.init();
Matrix.setIdentityM(mMVPMatrix, 0);
// 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;
shortBuffer = new ShortBuffer[rotateBuffers];
for (int i = 0; i < rotateBuffers; i++) {
ByteBuffer bbuf = ByteBuffer.allocateDirect(MB >> 2)
.order(ByteOrder.nativeOrder());
shortBuffer[i] = bbuf.asShortBuffer();
shortBuffer[i].put(mFillCoords, 0, 8);
}
}
public static void setRenderTheme(RenderTheme t) {
mClearColor = GlUtils.colorToFloat(t.getMapBackground());
mUpdateColor = true;
}
private static int uploadCnt = 0;
private static boolean uploadLayers(Layers layers, BufferObject vbo, boolean addFill) {
int newSize = layers.getSize();
if (newSize == 0) {
// Log.d(TAG, "empty");
return true;
}
GLES20.glBindBuffer(GL_ARRAY_BUFFER, vbo.id);
// use multiple buffers to avoid overwriting buffer while current
// data is uploaded (or rather the blocking which is probably done to
// avoid overwriting)
int curBuffer = uploadCnt++ % rotateBuffers;
//uploadCnt++;
ShortBuffer sbuf = shortBuffer[curBuffer];
// add fill coordinates
if (addFill)
newSize += 8;
if (sbuf.capacity() < newSize) {
sbuf = ByteBuffer
.allocateDirect(newSize * SHORT_BYTES)
.order(ByteOrder.nativeOrder())
.asShortBuffer();
shortBuffer[curBuffer] = sbuf;
} else {
sbuf.clear();
// if (addFill)
// sbuf.position(8);
}
if (addFill)
sbuf.put(mFillCoords, 0, 8);
layers.compile(sbuf, addFill);
sbuf.flip();
if (newSize != sbuf.remaining()) {
Log.d(TAG, "wrong size: "
+ newSize + " "
+ sbuf.position() + " "
+ sbuf.limit() + " "
+ sbuf.remaining());
return false;
}
newSize *= SHORT_BYTES;
// reuse memory allocated for vbo when possible and allocated
// memory is less then four times the new data
if (vbo.size > newSize && vbo.size < newSize * 4
&& mBufferMemoryUsage < LIMIT_BUFFERS) {
GLES20.glBufferSubData(GL_ARRAY_BUFFER, 0, newSize, sbuf);
} else {
mBufferMemoryUsage += newSize - vbo.size;
vbo.size = newSize;
GLES20.glBufferData(GL_ARRAY_BUFFER, vbo.size, sbuf, GL_DYNAMIC_DRAW);
//mBufferMemoryUsage += vbo.size;
}
return true;
}
private static void uploadTileData(MapTile tile) {
if (tile.layers == null) {
BufferObject.release(tile.vbo);
tile.vbo = null;
} else if (!uploadLayers(tile.layers, tile.vbo, true)) {
Log.d(TAG, "uploadTileData " + tile + " failed!");
tile.layers.clear();
tile.layers = null;
BufferObject.release(tile.vbo);
tile.vbo = null;
}
tile.state = STATE_READY;
}
private static boolean uploadOverlayData(RenderOverlay renderOverlay) {
if (uploadLayers(renderOverlay.layers, renderOverlay.vbo, true))
renderOverlay.isReady = true;
renderOverlay.newData = false;
return renderOverlay.isReady;
}
private static void checkBufferUsage(boolean force) {
// try to clear some unused vbo when exceding limit
if (!force && mBufferMemoryUsage < LIMIT_BUFFERS) {
if (CACHE_TILES < CACHE_TILES_MAX)
CACHE_TILES += 50;
return;
}
Log.d(TAG, "buffer object usage: " + mBufferMemoryUsage / MB + "MB");
mBufferMemoryUsage -= BufferObject.limitUsage(2 * MB);
Log.d(TAG, "now: " + mBufferMemoryUsage / MB + "MB");
if (mBufferMemoryUsage > LIMIT_BUFFERS && CACHE_TILES > 100)
CACHE_TILES -= 50;
}
@Override
public void onDrawFrame(GL10 glUnused) {
// prevent main thread recreating all tiles (updateMap)
// while rendering is going on.
drawlock.lock();
try {
draw();
} finally {
drawlock.unlock();
}
}
static void draw() {
long start = 0;
if (MapView.debugFrameTime)
start = SystemClock.uptimeMillis();
if (mUpdateColor) {
float cc[] = mClearColor;
GLES20.glClearColor(cc[0], cc[1], cc[2], cc[3]);
mUpdateColor = false;
}
// Note: it seems 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
GLES20.glDepthMask(true);
GLES20.glStencilMask(0xFF);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT
| GLES20.GL_DEPTH_BUFFER_BIT
| GLES20.GL_STENCIL_BUFFER_BIT);
int serial = 0;
if (mDrawTiles != null)
serial = mDrawTiles.serial;
// get current tiles to draw
mDrawTiles = TileManager.getActiveTiles(mDrawTiles);
// FIXME what if only drawing overlays?
if (mDrawTiles == null || mDrawTiles.cnt == 0) {
return;
}
boolean tilesChanged = false;
// check if the tiles have changed...
if (serial != mDrawTiles.serial) {
mMapPosition.zoomLevel = -1;
tilesChanged = true;
}
// get current MapPosition, set mTileCoords (mapping of screen to model
// coordinates)
MapPosition pos = mMapPosition;
float[] coords = mTileCoords;
boolean changed = mMapViewPosition.getMapPosition(pos, coords);
int tileCnt = mDrawTiles.cnt;
MapTile[] tiles = mDrawTiles.tiles;
if (changed) {
// get visible tiles
for (int i = 0; i < tileCnt; i++)
tiles[i].isVisible = false;
// relative zoom-level, 'tiles' could not have been updated after
// zoom-level changed.
byte z = tiles[0].zoomLevel;
float div = FastMath.pow(z - pos.zoomLevel);
// transform screen coordinates to tile coordinates
float scale = pos.scale / div;
float px = (float) pos.x * div;
float py = (float) pos.y * div;
for (int i = 0; i < 8; i += 2) {
coords[i + 0] = (px + coords[i + 0] / scale) / Tile.TILE_SIZE;
coords[i + 1] = (py + coords[i + 1] / scale) / Tile.TILE_SIZE;
}
mHolderCount = 0;
mScanBox.scan(coords, z);
}
tileCnt += mHolderCount;
/* compile layer data and upload to VBOs */
uploadCnt = 0;
for (int i = 0; i < tileCnt; i++) {
MapTile tile = tiles[i];
if (!tile.isVisible)
continue;
if (tile.state == STATE_NEW_DATA) {
uploadTileData(tile);
continue;
}
if (tile.holder != null) {
// load tile that is referenced by this holder
if (tile.holder.state == STATE_NEW_DATA)
uploadTileData(tile.holder);
tile.state = tile.holder.state;
} else if (tile.state != STATE_READY) {
// check near relatives than can serve as proxy
if ((tile.proxies & MapTile.PROXY_PARENT) != 0) {
MapTile rel = tile.rel.parent.tile;
if (rel.state == STATE_NEW_DATA)
uploadTileData(rel);
continue;
}
for (int c = 0; c < 4; c++) {
if ((tile.proxies & 1 << c) == 0)
continue;
MapTile rel = tile.rel.child[c].tile;
if (rel != null && rel.state == STATE_NEW_DATA)
uploadTileData(rel);
}
}
}
if (uploadCnt > 0)
checkBufferUsage(false);
tilesChanged |= (uploadCnt > 0);
/* update overlays */
List overlays = mMapView.getOverlayManager().getRenderLayers();
for (int i = 0, n = overlays.size(); i < n; i++)
overlays.get(i).update(mMapPosition, changed, tilesChanged);
/* draw base layer */
BaseLayer.draw(tiles, tileCnt, pos);
// start drawing while overlays uploading textures, etc
GLES20.glFlush();
/* draw overlays */
//GLState.blend(true);
GLES20.glEnable(GL_BLEND);
for (int i = 0, n = overlays.size(); i < n; i++) {
RenderOverlay renderOverlay = overlays.get(i);
if (renderOverlay.newData) {
if (renderOverlay.vbo == null) {
renderOverlay.vbo = BufferObject.get();
if (renderOverlay.vbo == null)
continue;
}
if (uploadOverlayData(renderOverlay))
renderOverlay.isReady = true;
}
if (renderOverlay.isReady) {
// setMatrix(mMVPMatrix, overlay);
renderOverlay.render(mMapPosition, mMVPMatrix, mProjMatrix);
}
}
if (MapView.debugFrameTime) {
GLES20.glFinish();
Log.d(TAG, "draw took " + (SystemClock.uptimeMillis() - start));
}
if (debugView) {
float mm = 0.5f;
float min = -mm;
float max = mm;
float ymax = mm * mHeight / mWidth;
mDebugCoords[0] = min;
mDebugCoords[1] = ymax;
mDebugCoords[2] = max;
mDebugCoords[3] = ymax;
mDebugCoords[4] = min;
mDebugCoords[5] = -ymax;
mDebugCoords[6] = max;
mDebugCoords[7] = -ymax;
PolygonRenderer.debugDraw(mProjMatrix, mDebugCoords, 0);
pos.zoomLevel = -1;
mMapViewPosition.getMapPosition(pos, mDebugCoords);
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0,
pos.viewMatrix, 0);
PolygonRenderer.debugDraw(mMVPMatrix, mDebugCoords, 1);
}
if (GlUtils.checkGlOutOfMemory("finish")) {
checkBufferUsage(true);
// TODO also throw out some textures etc
}
}
public static int depthOffset(MapTile t) {
return ((t.tileX % 4) + (t.tileY % 4 * 4) * 2) * 20;
}
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
Log.d(TAG, "SurfaceChanged:" + mNewSurface + " " + width + " " + height);
if (width <= 0 || height <= 0)
return;
mWidth = width;
mHeight = height;
GLES20.glScissor(0, 0, mWidth, mHeight);
float s = MapViewPosition.VIEW_SCALE;
float aspect = mHeight / (float) mWidth;
Matrix.frustumM(mProjMatrix, 0, -1 * s, 1 * s,
aspect * s, -aspect * s, MapViewPosition.VIEW_NEAR,
MapViewPosition.VIEW_FAR);
Matrix.setIdentityM(mTmpMatrix, 0);
Matrix.translateM(mTmpMatrix, 0, 0, 0, -MapViewPosition.VIEW_DISTANCE * 2);
Matrix.multiplyMM(mProjMatrix, 0, mProjMatrix, 0, mTmpMatrix, 0);
if (debugView) {
// modify this to scale only the view, to see better which tiles are
// rendered
Matrix.setIdentityM(mMVPMatrix, 0);
Matrix.scaleM(mMVPMatrix, 0, 0.5f, 0.5f, 1);
Matrix.multiplyMM(mProjMatrix, 0, mMVPMatrix, 0, mProjMatrix, 0);
}
BaseLayer.setProjection(mProjMatrix);
GLES20.glViewport(0, 0, width, height);
if (!mNewSurface) {
mMapView.redrawMap();
return;
}
mNewSurface = false;
mBufferMemoryUsage = 0;
mDrawTiles = null;
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));
BufferObject.init(numVBO);
// Set up textures
// TextRenderer.setup(numTiles);
if (mClearColor != null)
mUpdateColor = true;
GLState.init();
//vertexArray[0] = false;
//vertexArray[1] = false;
mMapView.redrawMap();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// String ext = GLES20.glGetString(GLES20.GL_EXTENSIONS);
// Log.d(TAG, "Extensions: " + ext);
LineRenderer.init();
PolygonRenderer.init();
TextureRenderer.init();
TextureObject.init(10);
GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
GLES20.glClearStencil(0);
GLES20.glDisable(GLES20.GL_CULL_FACE);
GLES20.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
mNewSurface = true;
}
private boolean mNewSurface;
private static final boolean debugView = false;
void clearBuffer() {
mNewSurface = true;
}
}