/* * 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; } }