package com.projectseptember.RNGL; import static android.opengl.GLES20.*; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.net.Uri; import android.opengl.GLSurfaceView; import android.util.DisplayMetrics; import android.view.ViewGroup; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.events.RCTEventEmitter; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class GLCanvas extends GLSurfaceView implements GLSurfaceView.Renderer, RunInGLThread { private ReactContext reactContext; private RNGLContext rnglContext; private boolean preloadingDone = false; private boolean deferredRendering = false; private GLRenderData renderData; private int defaultFBO; private int nbContentTextures; private boolean autoRedraw; private GLData data; private List imagesToPreload; private List preloaded = new ArrayList<>(); // FIXME double check that this works private Map images = new HashMap<>(); private List contentTextures = new ArrayList<>(); private List contentBitmaps = new ArrayList<>(); private Map shaders; private Map fbos; public GLCanvas(ThemedReactContext context) { super(context); reactContext = context; rnglContext = context.getNativeModule(RNGLContext.class); setEGLContextClientVersion(2); setEGLConfigChooser(8, 8, 8, 8, 16, 0); getHolder().setFormat(PixelFormat.RGB_888); setZOrderOnTop(true); setRenderer(this); setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); syncContentBitmaps(); requestRender(); } public GLFBO getFBO (Integer id) { if (!fbos.containsKey(id)) { fbos.put(id, new GLFBO()); } return fbos.get(id); } public GLShader getShader (Integer id) { if (!shaders.containsKey(id)) { GLShaderData shaderData = rnglContext.getShader(id); if (shaderData == null) return null; shaders.put(id, new GLShader(shaderData)); } return shaders.get(id); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { fbos = new HashMap<>(); shaders = new HashMap<>(); // TODO : need to reset GLImage and GLTexture. in a smart way (images if already loaded just need to re-set the bitmap) } @Override public void onSurfaceChanged(GL10 gl, int width, int height) {} @Override public void onDrawFrame(GL10 gl) { runAll(mRunOnDraw); if (contentTextures.size() != this.nbContentTextures) resizeUniformContentTextures(nbContentTextures); syncEventsThrough(); // FIXME, really need to do this ? if (!preloadingDone) { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); return; } final boolean shouldRenderNow = deferredRendering || autoRedraw || nbContentTextures == 0; if (nbContentTextures > 0) { reactContext.runOnUiQueueThread(new Runnable() { public void run() { syncContentBitmaps(); if (!deferredRendering) { deferredRendering = true; requestRender(); } } }); } if (shouldRenderNow) { this.render(); deferredRendering = false; } } public void setNbContentTextures(int n) { this.nbContentTextures = n; requestRender(); } public void setRenderId(int renderId) { if (nbContentTextures > 0) { if (preloadingDone) syncContentBitmaps(); requestRender(); } } public void setOpaque(boolean opaque) { if (opaque) { this.getHolder().setFormat(PixelFormat.RGB_888); } else { this.getHolder().setFormat(PixelFormat.TRANSLUCENT); } this.requestRender(); } public void setAutoRedraw(boolean autoRedraw) { this.autoRedraw = autoRedraw; this.setRenderMode(autoRedraw ? GLSurfaceView.RENDERMODE_CONTINUOUSLY : GLSurfaceView.RENDERMODE_WHEN_DIRTY); } public void setEventsThrough(boolean eventsThrough) { syncEventsThrough(); } public void setVisibleContent(boolean visibleContent) { syncEventsThrough(); } public void setCaptureNextFrameId(int captureNextFrameId) { // FIXME move away from this pattern. just use a method, same to ObjC impl this.requestRender(); } private boolean ensureCompiledShader (List data) { for (GLData d: data) { if (!ensureCompiledShader(d)) { return false; } } return true; } private boolean ensureCompiledShader (GLData data) { GLShader shader = getShader(data.shader); return shader != null && shader.ensureCompile() && ensureCompiledShader(data.children) && ensureCompiledShader(data.contextChildren); } public void setData (GLData data) { this.data = data; if (preloadingDone) syncContentBitmaps(); requestSyncData(); } public void setImagesToPreload (ReadableArray imagesToPreloadRA) { if (preloadingDone) return; List imagesToPreload = new ArrayList<>(); for (int i=0; i mRunOnDraw = new LinkedList<>(); public void runInGLThread (final Runnable runnable) { synchronized (mRunOnDraw) { mRunOnDraw.add(runnable); requestRender(); } } private void runAll(Queue queue) { synchronized (queue) { while (!queue.isEmpty()) { queue.poll().run(); } } } public void requestSyncData () { runInGLThread(new Runnable() { public void run() { if (ensureCompiledShader(data)) syncData(); else requestSyncData(); } }); } /** * Snapshot the content views and save to contentBitmaps (must run in UI Thread) */ public int syncContentBitmaps() { List bitmaps = new ArrayList<>(); ViewGroup parent = (ViewGroup) this.getParent(); int count = parent == null ? 0 : parent.getChildCount() - 1; for (int i = 0; i < count; i++) { bitmaps.add(GLTexture.captureView(parent.getChildAt(i))); } contentBitmaps = bitmaps; //Log.i("GLCanvas", "syncContentBitmaps "+count+" "+parent); return count; } /** * Draw contentBitmaps to contentTextures (must run in GL Thread) */ public int syncContentTextures() { int size = Math.min(contentTextures.size(), contentBitmaps.size()); for (int i=0; i images) { Map prevImages = this.images; GLShader shader = getShader(data.shader); Map uniformsInteger = new HashMap<>(); Map uniformsFloat = new HashMap<>(); Map uniformsIntBuffer = new HashMap<>(); Map uniformsFloatBuffer = new HashMap<>(); Map textures = new HashMap<>(); List contextChildren = new ArrayList<>(); List children = new ArrayList<>(); for (GLData child: data.contextChildren) { contextChildren.add(recSyncData(child, images)); } for (GLData child: data.children) { children.add(recSyncData(child, images)); } Map uniformTypes = shader.getUniformTypes(); int units = 0; ReadableMapKeySetIterator iterator = data.uniforms.keySetIterator(); while (iterator.hasNextKey()) { String uniformName = iterator.nextKey(); int type = uniformTypes.get(uniformName); ReadableMap dataUniforms = data.uniforms; if (type == GL_SAMPLER_2D || type == GL_SAMPLER_CUBE) { uniformsInteger.put(uniformName, units++); if (dataUniforms.isNull(uniformName)) { GLTexture emptyTexture = new GLTexture(); emptyTexture.setPixelsEmpty(); textures.put(uniformName, emptyTexture); } else { // FIXME: in case of require() it's now a number... // TODO: need to support this. as well as on iOS side ReadableMap value = dataUniforms.getMap(uniformName); String t = value.getString("type"); if (t.equals("content")) { int id = value.getInt("id"); if (id >= contentTextures.size()) { resizeUniformContentTextures(id+1); } textures.put(uniformName, contentTextures.get(id)); } else if (t.equals("fbo")) { int id = value.getInt("id"); GLFBO fbo = getFBO(id); textures.put(uniformName, fbo.color.get(0)); } else if (t.equals("uri")) { final Uri src = srcResource(value); if (src == null) { shader.runtimeException("texture uniform '"+uniformName+"': Invalid uri format '"+value+"'"); } GLImage image = images.get(src); if (image == null) { image = prevImages.get(src); if (image != null) images.put(src, image); } if (image == null) { image = new GLImage(reactContext.getApplicationContext(), this, new Runnable() { public void run() { onImageLoad(src); } }); image.setSrc(src); images.put(src, image); } textures.put(uniformName, image.getTexture()); } else { shader.runtimeException("texture uniform '" + uniformName + "': Unexpected type '" + type + "'"); } } } else { switch (type) { case GL_INT: uniformsInteger.put(uniformName, dataUniforms.getInt(uniformName)); break; case GL_BOOL: uniformsInteger.put(uniformName, dataUniforms.getBoolean(uniformName) ? 1 : 0); break; case GL_FLOAT: uniformsFloat.put(uniformName, (float) dataUniforms.getDouble(uniformName)); break; case GL_FLOAT_VEC2: case GL_FLOAT_VEC3: case GL_FLOAT_VEC4: case GL_FLOAT_MAT2: case GL_FLOAT_MAT3: case GL_FLOAT_MAT4: ReadableArray arr = dataUniforms.getArray(uniformName); if (arraySizeForType(type) != arr.size()) { shader.runtimeException( "uniform '"+uniformName+ "': Invalid array size: "+arr.size()+ ". Expected: "+arraySizeForType(type)); } uniformsFloatBuffer.put(uniformName, parseAsFloatArray(arr)); break; case GL_INT_VEC2: case GL_INT_VEC3: case GL_INT_VEC4: case GL_BOOL_VEC2: case GL_BOOL_VEC3: case GL_BOOL_VEC4: ReadableArray arr2 = dataUniforms.getArray(uniformName); if (arraySizeForType(type) != arr2.size()) { shader.runtimeException( "uniform '"+uniformName+ "': Invalid array size: "+arr2.size()+ ". Expected: "+arraySizeForType(type)); } uniformsIntBuffer.put(uniformName, parseAsIntArray(arr2)); break; default: shader.runtimeException( "uniform '"+uniformName+ "': type not supported: "+type); } } } int[] maxTextureUnits = new int[1]; glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, maxTextureUnits, 0); if (units > maxTextureUnits[0]) { shader.runtimeException("Maximum number of texture reach. got " + units + " >= max " + maxTextureUnits); } for (String uniformName: uniformTypes.keySet()) { if (!uniformsFloat.containsKey(uniformName) && !uniformsInteger.containsKey(uniformName) && !uniformsFloatBuffer.containsKey(uniformName) && !uniformsIntBuffer.containsKey(uniformName)) { shader.runtimeException("All defined uniforms must be provided. Missing '"+uniformName+"'"); } } return new GLRenderData( shader, uniformsInteger, uniformsFloat, uniformsIntBuffer, uniformsFloatBuffer, textures, data.width, data.height, data.fboId, contextChildren, children); } private FloatBuffer parseAsFloatArray(ReadableArray array) { int size = array.size(); FloatBuffer buf = ByteBuffer.allocateDirect(size * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); for (int i=0; i images = new HashMap<>(); renderData = recSyncData(data, images); this.images = images; } public void recRender (GLRenderData renderData) { DisplayMetrics dm = reactContext.getResources().getDisplayMetrics(); int w = Float.valueOf(renderData.width.floatValue() * dm.density).intValue(); int h = Float.valueOf(renderData.height.floatValue() * dm.density).intValue(); for (GLRenderData child: renderData.contextChildren) recRender(child); for (GLRenderData child: renderData.children) recRender(child); if (renderData.fboId == -1) { glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); glViewport(0, 0, w, h); } else { GLFBO fbo = getFBO(renderData.fboId); fbo.setShape(w, h); fbo.bind(); } renderData.shader.bind(); for (String uniformName: renderData.textures.keySet()) { GLTexture texture = renderData.textures.get(uniformName); int unit = renderData.uniformsInteger.get(uniformName); texture.bind(unit); } Map uniformTypes = renderData.shader.getUniformTypes(); for (String uniformName: renderData.uniformsInteger.keySet()) { renderData.shader.setUniform(uniformName, renderData.uniformsInteger.get(uniformName)); } for (String uniformName: renderData.uniformsFloat.keySet()) { renderData.shader.setUniform(uniformName, renderData.uniformsFloat.get(uniformName)); } for (String uniformName: renderData.uniformsFloatBuffer.keySet()) { renderData.shader.setUniform(uniformName, renderData.uniformsFloatBuffer.get(uniformName), uniformTypes.get(uniformName)); } for (String uniformName: renderData.uniformsIntBuffer.keySet()) { renderData.shader.setUniform(uniformName, renderData.uniformsIntBuffer.get(uniformName), uniformTypes.get(uniformName)); } glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, 6); } public void render () { if (renderData == null) return; syncContentTextures(); int[] defaultFBOArr = new int[1]; glGetIntegerv(GL_FRAMEBUFFER_BINDING, defaultFBOArr, 0); defaultFBO = defaultFBOArr[0]; glEnable(GL_BLEND); recRender(renderData); glDisable(GL_BLEND); glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); } public void syncEventsThrough () { // TODO: figure out how to do this... // For some reason, the click through is half working } private void dispatchOnProgress(double progress, int count, int total) { WritableMap event = Arguments.createMap(); event.putDouble("progress", progress); event.putInt("count", count); event.putInt("total", total); ReactContext reactContext = (ReactContext)getContext(); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( getId(), "progress", event); } public void dispatchOnLoad () { WritableMap event = Arguments.createMap(); ReactContext reactContext = (ReactContext)getContext(); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( getId(), "load", event); } }